From e39ff9289490271a43927b44a44d617e237e0688 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 14 Jun 2025 16:45:08 +0200 Subject: [PATCH 01/37] fix comment trustwallet is the one having limited methods support and doesn't support eth_signTransaction. Metamask supports nearly all methods --- mm2src/coins/eth.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 91c3294c2d..8b7066c982 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2872,8 +2872,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) From facae9de0fb0847e6ce47bdd13806a870267dfb1 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 14 Jun 2025 20:42:35 +0200 Subject: [PATCH 02/37] add a todo regarind session proposal response not relevant for now till KDF could be used as a wallet --- mm2src/kdf_walletconnect/src/session/rpc/propose.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mm2src/kdf_walletconnect/src/session/rpc/propose.rs b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs index d3626430ce..35e5475fe1 100644 --- a/mm2src/kdf_walletconnect/src/session/rpc/propose.rs +++ b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs @@ -66,6 +66,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) From 638fe3fd43c791851d0d8df96596133eed723bb0 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 14 Jun 2025 20:59:00 +0200 Subject: [PATCH 03/37] add a todo related to ledger connection detection in wallet connect --- mm2src/kdf_walletconnect/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs index 8f8740e081..a2ac989839 100644 --- a/mm2src/kdf_walletconnect/src/lib.rs +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -492,6 +492,7 @@ impl WalletConnectCtxImpl { .get_session(&session_topic) .and_then(|session| session.session_properties) .and_then(|props| props.keys.as_ref().cloned()) + // TODO: Please re-check if this is correct. Why do we pick the first KeyInfo? shouldn't we pick the one with matching chain_id? .and_then(|keys| keys.first().cloned()) .map(|key| key.is_nano_ledger) .unwrap_or(false) From a5b5909d30309cacc9843b333a477b055efdf952 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 14 Jun 2025 23:03:03 +0200 Subject: [PATCH 04/37] rename send_proposal_request for clarity and add a todo --- mm2src/kdf_walletconnect/src/lib.rs | 10 ++++++++-- mm2src/kdf_walletconnect/src/session/rpc/propose.rs | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs index a2ac989839..133b923ba1 100644 --- a/mm2src/kdf_walletconnect/src/lib.rs +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -8,7 +8,7 @@ pub mod session; mod storage; use crate::connection_handler::{handle_disconnections, 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; @@ -258,7 +258,13 @@ impl WalletConnectCtxImpl { Ok(res) => { res.map_to_mm(|err| err.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?; return Ok(url); }, Err(_) => self.wait_until_client_is_online_loop(attempt).await, diff --git a/mm2src/kdf_walletconnect/src/session/rpc/propose.rs b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs index 35e5475fe1..893ea0831d 100644 --- a/mm2src/kdf_walletconnect/src/session/rpc/propose.rs +++ b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs @@ -14,7 +14,7 @@ use relay_rpc::{domain::{MessageId, Topic}, RequestParams, ResponseParamsSuccess}}; /// 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, From ab0edba63c863d5a036303726c6f33caa9032af1 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sun, 15 Jun 2025 17:29:44 +0200 Subject: [PATCH 05/37] add a todo regarding a possible mis-use of session properties --- mm2src/kdf_walletconnect/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs index 133b923ba1..fd1e873316 100644 --- a/mm2src/kdf_walletconnect/src/lib.rs +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -528,6 +528,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) { From 42ddeb1324a1d954d58cd63a8add6d32107d4ad7 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sun, 15 Jun 2025 21:52:07 +0200 Subject: [PATCH 06/37] rename PersonalSign to EthPersonalSign to signify it's eth specific method --- mm2src/coins/eth/wallet_connect.rs | 2 +- mm2src/kdf_walletconnect/src/chain.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/eth/wallet_connect.rs b/mm2src/coins/eth/wallet_connect.rs index 2bc241342e..c95567ba91 100644 --- a/mm2src/coins/eth/wallet_connect.rs +++ b/mm2src/coins/eth/wallet_connect.rs @@ -201,7 +201,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?; Ok(extract_pubkey_from_signature(&data, message, &account_str) diff --git a/mm2src/kdf_walletconnect/src/chain.rs b/mm2src/kdf_walletconnect/src/chain.rs index 20e1acd6a8..65bf6a5beb 100644 --- a/mm2src/kdf_walletconnect/src/chain.rs +++ b/mm2src/kdf_walletconnect/src/chain.rs @@ -90,7 +90,7 @@ pub enum WcRequestMethods { CosmosGetAccounts, EthSignTransaction, EthSendTransaction, - PersonalSign, + EthPersonalSign, } impl AsRef for WcRequestMethods { @@ -101,7 +101,7 @@ impl AsRef for WcRequestMethods { Self::CosmosGetAccounts => "cosmos_getAccounts", Self::EthSignTransaction => "eth_signTransaction", Self::EthSendTransaction => "eth_sendTransaction", - Self::PersonalSign => "personal_sign", + Self::EthPersonalSign => "personal_sign", } } } From fd0c6abd36a2f3aa0fc74d03c42b2fd1f634941f Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sun, 15 Jun 2025 22:35:33 +0200 Subject: [PATCH 07/37] add btc/utxo wallet connect method names --- mm2src/kdf_walletconnect/src/chain.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mm2src/kdf_walletconnect/src/chain.rs b/mm2src/kdf_walletconnect/src/chain.rs index 65bf6a5beb..0b3c34bda7 100644 --- a/mm2src/kdf_walletconnect/src/chain.rs +++ b/mm2src/kdf_walletconnect/src/chain.rs @@ -91,6 +91,15 @@ pub enum WcRequestMethods { EthSignTransaction, EthSendTransaction, EthPersonalSign, + // Notes (remove 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, } impl AsRef for WcRequestMethods { @@ -102,6 +111,9 @@ impl AsRef for WcRequestMethods { Self::EthSignTransaction => "eth_signTransaction", Self::EthSendTransaction => "eth_sendTransaction", Self::EthPersonalSign => "personal_sign", + Self::UtxoGetAccountAddresses => "getAccountAddresses", + Self::UtxoSendTransfer => "sendTransfer", + Self::UtxoSignPsbt => "signPsbt", } } } From 803d8f52bb159318d5af6f7e6ea2a77b363eafa3 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Mon, 16 Jun 2025 13:58:42 +0200 Subject: [PATCH 08/37] recognize that the conversion from PrivKeyBuildPolicy to the eth counterpart might be fallible we assumed it's infallible yet we used try_from in eth_coin_from_conf_and_request :/ anyway, we want this to be fallible since we will later add WalletConnect to PrivKeyBuildPolicy and the conversion in this case might not always succeed --- mm2src/coins/eth.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 8b7066c982..c00c32c10c 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -836,12 +836,14 @@ impl EthPrivKeyBuildPolicy { } } -impl From for EthPrivKeyBuildPolicy { - fn from(policy: PrivKeyBuildPolicy) -> Self { +impl TryFrom for EthPrivKeyBuildPolicy { + type Error = String; + + fn try_from(policy: PrivKeyBuildPolicy) -> Result { match policy { - PrivKeyBuildPolicy::IguanaPrivKey(iguana) => EthPrivKeyBuildPolicy::IguanaPrivKey(iguana), - PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd), - PrivKeyBuildPolicy::Trezor => EthPrivKeyBuildPolicy::Trezor, + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(iguana)), + PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd)), + PrivKeyBuildPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), } } } @@ -6492,8 +6494,19 @@ pub async fn eth_coin_from_conf_and_request( } } - // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. + // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy`. let priv_key_policy = try_s!(EthPrivKeyBuildPolicy::try_from(priv_key_policy)); + // Make sure not to allow new activation methods that are not supported for legacy ETH coin activation. + match priv_key_policy { + EthPrivKeyBuildPolicy::WalletConnect { .. } => { + return ERR!("WalletConnect private key policy is not supported for legacy ETH coin activation"); + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyBuildPolicy::Metamask { .. } => { + return ERR!("Metamask 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() { @@ -6795,7 +6808,8 @@ pub async fn get_eth_address( } else { PrivKeyBuildPolicy::detect_priv_key_policy(ctx)? } - .into(); + .try_into() + .map_err(GetEthAddressError::Internal)?; let (_, derivation_method) = build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None).await?; From 9799ec90bab79d801a07faba69ee93fb49c2dd2c Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Tue, 17 Jun 2025 22:52:08 +0200 Subject: [PATCH 09/37] add a PrivKeyBuildPolicy::WalletConnect to be used for UTXO even tho this struct is intertwined with eth and tendermint code. it's actually never utilized there :/ --- mm2src/coins/eth.rs | 9 +++++++++ mm2src/coins/lp_coins.rs | 17 ++++++++++++++++- mm2src/coins/qrc20.rs | 6 ++++++ mm2src/coins/tendermint/tendermint_coin.rs | 9 +++++++++ .../utxo/utxo_builder/utxo_coin_builder.rs | 4 ++++ mm2src/coins/z_coin.rs | 7 +++++++ .../src/utxo_activation/common_impl.rs | 6 +++++- .../src/utxo_activation/init_bch_activation.rs | 2 +- .../src/utxo_activation/init_qtum_activation.rs | 2 +- .../init_utxo_standard_activation.rs | 2 +- 10 files changed, 59 insertions(+), 5 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index c00c32c10c..8d05e12e16 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -844,6 +844,15 @@ impl TryFrom for EthPrivKeyBuildPolicy { PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(iguana)), PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd)), PrivKeyBuildPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), + PrivKeyBuildPolicy::WalletConnect { + address, + public_key_uncompressed, + session_topic, + } => Ok(EthPrivKeyBuildPolicy::WalletConnect { + address: valid_addr_from_str(&address)?, + public_key_uncompressed, + session_topic, + }), } } } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index c0414335f5..ac0d14320f 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -4157,11 +4157,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: String, + }, } impl PrivKeyActivationPolicy { @@ -4217,6 +4220,12 @@ 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. + // FIXME: 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, @@ -4359,6 +4368,12 @@ pub enum PrivKeyBuildPolicy { IguanaPrivKey(IguanaPrivKey), GlobalHDAccount(GlobalHDAccountArc), Trezor, + // FIXME: Let this only include the session_topic as this struct is a `Build` version. Do the same thing for EthPrivKeyBuildPolicy. + WalletConnect { + address: String, + public_key_uncompressed: H520, + session_topic: String, + }, } impl PrivKeyBuildPolicy { diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 3d6f8660b3..66a38813fd 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -290,6 +290,12 @@ impl<'a> UtxoCoinBuilder for Qrc20CoinBuilder<'a> { let priv_key_err = PrivKeyPolicyNotAllowed::HardwareWalletNotSupported; return MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err)); }, + PrivKeyBuildPolicy::WalletConnect { .. } => { + let priv_key_err = PrivKeyPolicyNotAllowed::UnsupportedMethod( + "WalletConnect is not available for QRC20 coin".to_string(), + ); + return MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err)); + }, }; let inner = Qrc20CoinFields { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 8e4934201b..6c94b8d763 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -4143,6 +4143,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/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 03ccd6fd8b..8f9a345b83 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -131,6 +131,10 @@ pub trait UtxoCoinBuilder: self.build_utxo_fields_with_global_hd(global_hd_ctx).await }, PrivKeyBuildPolicy::Trezor => self.build_utxo_fields_with_trezor().await, + PrivKeyBuildPolicy::WalletConnect { .. } => { + // FIXME: don't panic. + panic!("for now"); + }, } } } diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index e31a8f8fa0..bf9fe310b6 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1983,6 +1983,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_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index eba196c370..512c4f72a2 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -88,11 +88,15 @@ 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 { .. } => { + // FIXME: don't panic. + panic!("for now"); + }, } } 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 8c27226959..a89b289ac8 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs @@ -77,7 +77,7 @@ impl InitStandaloneCoinActivationOps for BchCoin { error: format!("Couldn't parse cash address prefix: {e:?}"), } })?; - let priv_key_policy = priv_key_build_policy(&ctx, activation_request.utxo_params.priv_key_policy)?; + let priv_key_policy = priv_key_build_policy(&ctx, &activation_request.utxo_params.priv_key_policy)?; 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 a6644f3275..b6bb512b27 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs @@ -62,7 +62,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)?; + let priv_key_policy = priv_key_build_policy(&ctx, &activation_request.priv_key_policy)?; 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 10715e2f0e..73f28e564a 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 @@ -63,7 +63,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)?; + let priv_key_policy = priv_key_build_policy(&ctx, &activation_request.priv_key_policy)?; let coin = UtxoArcBuilder::new( &ctx, From 72e845b861053a11297064367bf20801d4670cc9 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 18 Jun 2025 12:45:33 +0200 Subject: [PATCH 10/37] dont store address not pubkey in EthPrivKeyBuildPolicy::WalletConnect --- mm2src/coins/eth.rs | 22 ++++++------------ mm2src/coins/eth/v2_activation.rs | 19 +++++++++++---- .../src/eth_with_token_activation.rs | 23 ++++--------------- 3 files changed, 25 insertions(+), 39 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 8d05e12e16..e8d531c95d 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -104,7 +104,6 @@ use web3::{self, Web3}; cfg_wasm32! { use crypto::MetamaskArc; - use ethereum_types::H520; use mm2_metamask::MetamaskError; use web3::types::TransactionRequest; } @@ -814,8 +813,6 @@ pub enum EthPrivKeyBuildPolicy { Metamask(MetamaskArc), Trezor, WalletConnect { - address: Address, - public_key_uncompressed: H520, session_topic: String, }, } @@ -844,15 +841,9 @@ impl TryFrom for EthPrivKeyBuildPolicy { PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(iguana)), PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd)), PrivKeyBuildPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), - PrivKeyBuildPolicy::WalletConnect { - address, - public_key_uncompressed, - session_topic, - } => Ok(EthPrivKeyBuildPolicy::WalletConnect { - address: valid_addr_from_str(&address)?, - public_key_uncompressed, - session_topic, - }), + PrivKeyBuildPolicy::WalletConnect { session_topic, .. } => { + Ok(EthPrivKeyBuildPolicy::WalletConnect { session_topic }) + }, } } } @@ -6540,8 +6531,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()); @@ -6821,7 +6813,7 @@ pub async fn get_eth_address( .map_err(GetEthAddressError::Internal)?; let (_, derivation_method) = - build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None).await?; + build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None, None).await?; let my_address = derivation_method.single_addr_or_err().await?; Ok(MyWalletAddress { diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 9bcd2d6c57..ecc8bc2277 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, HDWalletStorageError, DEFAULT_GAP_LIMIT}; @@ -635,6 +636,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?; @@ -735,6 +737,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) => { @@ -825,11 +828,17 @@ 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_id = chain_spec + .ok_or(EthActivationV2Error::ChainIdNotSet)? + .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()))?; let public_key = compress_public_key(public_key_uncompressed)?; Ok(( EthPrivKeyPolicy::WalletConnect { diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 95b74f71a4..2dadb6d60a 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -12,14 +12,12 @@ use coins::coin_balance::{CoinBalanceReport, EnableCoinBalanceOps}; 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; use coins::nft::nft_structs::NftInfo; use coins::{CoinBalance, CoinBalanceMap, CoinProtocol, CoinWithDerivationMethod, DerivationMethod, MarketCoinOps, MmCoin, MmCoinEnum}; -use kdf_walletconnect::WalletConnectCtx; use crate::platform_coin_with_tokens::InitPlatformCoinWithTokensTask; use common::Future01CompatExt; @@ -286,7 +284,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, @@ -471,7 +469,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 => Ok(EthPrivKeyBuildPolicy::detect_priv_key_policy(ctx)?), @@ -483,20 +480,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(), + }), } } From 9a03495d420741262d42287dc25892510e370702 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 18 Jun 2025 12:50:41 +0200 Subject: [PATCH 11/37] dont store address not pubkey in PrivKeyBuildPolicy::WalletConnect --- mm2src/coins/eth.rs | 2 +- mm2src/coins/lp_coins.rs | 7 +------ mm2src/coins_activation/src/utxo_activation/common_impl.rs | 7 +++---- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index e8d531c95d..ba00d4d64c 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -841,7 +841,7 @@ impl TryFrom for EthPrivKeyBuildPolicy { PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(iguana)), PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd)), PrivKeyBuildPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), - PrivKeyBuildPolicy::WalletConnect { session_topic, .. } => { + PrivKeyBuildPolicy::WalletConnect { session_topic } => { Ok(EthPrivKeyBuildPolicy::WalletConnect { session_topic }) }, } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index ac0d14320f..265fc2ad12 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -4368,12 +4368,7 @@ pub enum PrivKeyBuildPolicy { IguanaPrivKey(IguanaPrivKey), GlobalHDAccount(GlobalHDAccountArc), Trezor, - // FIXME: Let this only include the session_topic as this struct is a `Build` version. Do the same thing for EthPrivKeyBuildPolicy. - WalletConnect { - address: String, - public_key_uncompressed: H520, - session_topic: String, - }, + WalletConnect { session_topic: String }, } impl PrivKeyBuildPolicy { diff --git a/mm2src/coins_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index 512c4f72a2..71a72a6926 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -93,10 +93,9 @@ pub(crate) fn priv_key_build_policy( match activation_policy { PrivKeyActivationPolicy::ContextPrivKey => PrivKeyBuildPolicy::detect_priv_key_policy(ctx), PrivKeyActivationPolicy::Trezor => Ok(PrivKeyBuildPolicy::Trezor), - PrivKeyActivationPolicy::WalletConnect { .. } => { - // FIXME: don't panic. - panic!("for now"); - }, + PrivKeyActivationPolicy::WalletConnect { session_topic } => Ok(PrivKeyBuildPolicy::WalletConnect { + session_topic: session_topic.clone(), + }), } } From 3769d41dd420239e2eafdafdbf39b14d989b203b Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 18 Jun 2025 15:30:27 +0200 Subject: [PATCH 12/37] free UtxoCoinBuilder of the trait hell having these weird one func trait made a messy trace all over. coins who don't even support hw wallet had to implement the trait for hw wallet builder and not use it. --- mm2src/coins/qrc20.rs | 31 +- mm2src/coins/utxo/qtum.rs | 10 +- mm2src/coins/utxo/utxo_builder/mod.rs | 6 +- .../utxo/utxo_builder/utxo_arc_builder.rs | 19 +- .../utxo/utxo_builder/utxo_coin_builder.rs | 351 +++++++++--------- mm2src/coins/z_coin.rs | 11 +- 6 files changed, 185 insertions(+), 243 deletions(-) diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 66a38813fd..64ec10a0d8 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -8,9 +8,8 @@ use crate::utxo::rpc_clients::{ElectrumClient, NativeClient, UnspentInfo, UtxoRp UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; #[cfg(not(target_arch = "wasm32"))] use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; -use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, - UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, - UtxoFieldsWithIguanaSecretBuilder}; +use crate::utxo::utxo_builder::{build_utxo_fields_with_global_hd, build_utxo_fields_with_iguana_secret, + 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::{qtum, ActualFeeRate, AddrFromStrError, BroadcastTxErr, FeePolicy, GenerateTxError, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, UnsupportedAddr, @@ -265,14 +264,6 @@ impl<'a> UtxoCoinBuilderCommonOps for Qrc20CoinBuilder<'a> { } } -impl<'a> UtxoFieldsWithIguanaSecretBuilder for Qrc20CoinBuilder<'a> {} - -impl<'a> UtxoFieldsWithGlobalHDBuilder for Qrc20CoinBuilder<'a> {} - -/// Although, `Qrc20Coin` doesn't support [`PrivKeyBuildPolicy::Trezor`] yet, -/// `UtxoCoinBuilder` trait requires `UtxoFieldsWithHardwareWalletBuilder` to be implemented. -impl<'a> UtxoFieldsWithHardwareWalletBuilder for Qrc20CoinBuilder<'a> {} - #[async_trait] impl<'a> UtxoCoinBuilder for Qrc20CoinBuilder<'a> { type ResultCoin = Qrc20Coin; @@ -280,23 +271,27 @@ impl<'a> UtxoCoinBuilder for Qrc20CoinBuilder<'a> { fn priv_key_policy(&self) -> PrivKeyBuildPolicy { 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_secret(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(), ); - return MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err)); + MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err)) }, - }; + } + } + + async fn build(self) -> MmResult { + let utxo = self.build_utxo_fields().await?; let inner = Qrc20CoinFields { utxo, diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index a45b4a9491..2f36b8a95b 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -17,9 +17,7 @@ use crate::rpc_command::init_scan_for_new_addresses::{self, InitScanAddressesRpc ScanAddressesResponse}; 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}; @@ -201,12 +199,6 @@ impl<'a> UtxoCoinBuilderCommonOps for QtumCoinBuilder<'a> { fn check_utxo_maturity(&self) -> bool { self.activation_params().check_utxo_maturity.unwrap_or(true) } } -impl<'a> UtxoFieldsWithIguanaSecretBuilder for QtumCoinBuilder<'a> {} - -impl<'a> UtxoFieldsWithGlobalHDBuilder for QtumCoinBuilder<'a> {} - -impl<'a> UtxoFieldsWithHardwareWalletBuilder for QtumCoinBuilder<'a> {} - #[async_trait] impl<'a> UtxoCoinBuilder for QtumCoinBuilder<'a> { type ResultCoin = QtumCoin; diff --git a/mm2src/coins/utxo/utxo_builder/mod.rs b/mm2src/coins/utxo/utxo_builder/mod.rs index 205c36ee71..bd84d08012 100644 --- a/mm2src/coins/utxo/utxo_builder/mod.rs +++ b/mm2src/coins/utxo/utxo_builder/mod.rs @@ -3,9 +3,9 @@ mod utxo_coin_builder; 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}; +pub use utxo_coin_builder::{build_utxo_fields_with_global_hd, build_utxo_fields_with_iguana_secret, + UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, + DAY_IN_SECONDS}; pub use utxo_conf_builder::{UtxoConfBuilder, UtxoConfError, UtxoConfResult}; #[cfg(test)] diff --git a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs index 65bd34103f..1a5b63d7c5 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs @@ -1,9 +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}; use crate::{DerivationMethod, PrivKeyBuildPolicy, UtxoActivationParams}; @@ -78,21 +76,6 @@ where fn ticker(&self) -> &str { self.ticker } } -impl<'a, F, T> UtxoFieldsWithIguanaSecretBuilder for UtxoArcBuilder<'a, F, T> where - F: Fn(UtxoArc) -> T + Send + Sync + 'static -{ -} - -impl<'a, F, T> UtxoFieldsWithGlobalHDBuilder for UtxoArcBuilder<'a, F, T> where - F: Fn(UtxoArc) -> T + Send + Sync + 'static -{ -} - -impl<'a, F, T> UtxoFieldsWithHardwareWalletBuilder for UtxoArcBuilder<'a, F, T> where - F: Fn(UtxoArc) -> T + Send + Sync + 'static -{ -} - #[async_trait] impl<'a, F, T> UtxoCoinBuilder for UtxoArcBuilder<'a, 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 8f9a345b83..64cfdfbe48 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -23,7 +23,6 @@ pub use keys::{Address, AddressBuilder, AddressFormat as UtxoAddressFormat, Addr KeyPair, Private, Public, Secret}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use primitives::hash::H160; use serde_json::{self as json, Value as Json}; use spv_validation::conf::SPVConf; use spv_validation::helpers_validation::SPVError; @@ -114,9 +113,7 @@ impl From for UtxoCoinBuildError { } #[async_trait] -pub trait UtxoCoinBuilder: - UtxoFieldsWithIguanaSecretBuilder + UtxoFieldsWithGlobalHDBuilder + UtxoFieldsWithHardwareWalletBuilder -{ +pub trait UtxoCoinBuilder: UtxoCoinBuilderCommonOps { type ResultCoin; type Error: NotMmError; @@ -126,11 +123,11 @@ 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_secret(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 => self.build_utxo_fields_with_trezor().await, + PrivKeyBuildPolicy::Trezor => build_utxo_fields_with_trezor(self).await, PrivKeyBuildPolicy::WalletConnect { .. } => { // FIXME: don't panic. panic!("for now"); @@ -139,94 +136,92 @@ pub trait UtxoCoinBuilder: } } -#[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()?; - 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()) - .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 - } +pub async fn build_utxo_fields_with_iguana_secret( + builder: &Builder, + priv_key: IguanaPrivKey, +) -> UtxoCoinBuildResult +where + Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, +{ + let conf = UtxoConfBuilder::new(builder.conf(), builder.activation_params(), builder.ticker()).build()?; + 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 = builder.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()) + .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()?; - - 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 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).await?; - 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 - } +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()?; + + let path_to_address = builder.activation_params().path_to_address; + let path_to_coin = conf + .derivation_path + .as_ref() + .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet)?; + 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(), + }; - fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } + 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?; + 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_coin_fields_with_conf_and_policy( @@ -290,108 +285,94 @@ 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()?; - - 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)?; - - let hd_wallet_storage = HDWalletCoinStorage::init(self.ctx(), ticker).await?; - - 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, - }; +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()?; - let my_address = hd_wallet - .get_enabled_address() - .await - .ok_or_else(|| UtxoCoinBuildError::Internal("Failed to get enabled address from HD wallet".to_owned()))?; - let my_script_pubkey = output_script(&my_address.address).map(|script| script.to_bytes())?; - let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)); - - // 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) + // Make sure this coin supports Trezor. + if conf.trezor_coin.is_none() { + return MmError::err(UtxoCoinBuildError::CoinDoesntSupportTrezor); } - fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } - - fn supports_trezor(&self, conf: &UtxoCoinConf) -> bool { conf.trezor_coin.is_some() } - - fn trezor_wallet_rmd160(&self) -> UtxoCoinBuildResult { - let crypto_ctx = CryptoCtx::from_ctx(self.ctx())?; + let hd_wallet_rmd160 = { + let crypto_ctx = CryptoCtx::from_ctx(builder.ctx())?; 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())?; - 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)?; + + let hd_wallet_storage = HDWalletCoinStorage::init(builder.ctx(), ticker).await?; + + 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 my_address = hd_wallet + .get_enabled_address() + .await + .ok_or_else(|| UtxoCoinBuildError::Internal("Failed to get enabled address from HD wallet".to_owned()))?; + let my_script_pubkey = output_script(&my_address.address).map(|script| script.to_bytes())?; + let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)); + + // 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] diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index bf9fe310b6..61c34c4302 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -15,8 +15,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; -use crate::utxo::utxo_builder::{UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, - UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; +use crate::utxo::utxo_builder::{UtxoCoinBuilder, UtxoCoinBuilderCommonOps}; use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat}; use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, payment_script}; use crate::utxo::{sat_from_big_decimal, utxo_common, ActualFeeRate, AdditionalTxData, AddrFromStrError, Address, @@ -883,14 +882,6 @@ impl<'a> UtxoCoinBuilderCommonOps for ZCoinBuilder<'a> { fn ticker(&self) -> &str { self.ticker } } -impl<'a> UtxoFieldsWithIguanaSecretBuilder for ZCoinBuilder<'a> {} - -impl<'a> UtxoFieldsWithGlobalHDBuilder for ZCoinBuilder<'a> {} - -/// Although, `ZCoin` doesn't support [`PrivKeyBuildPolicy::Trezor`] yet, -/// `UtxoCoinBuilder` trait requires `UtxoFieldsWithHardwareWalletBuilder` to be implemented. -impl<'a> UtxoFieldsWithHardwareWalletBuilder for ZCoinBuilder<'a> {} - #[async_trait] impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { type ResultCoin = ZCoin; From bc796a242d1920dd5843a1e75093915af29db19b Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 21 Jun 2025 22:21:06 +0200 Subject: [PATCH 13/37] impl wallet connection activation for btc not btc, but rather any utxo that has a caip bip122 chain_id and is supported by the wallet side --- mm2src/coins/lp_coins.rs | 12 +-- mm2src/coins/utxo.rs | 1 + .../utxo/utxo_builder/utxo_coin_builder.rs | 94 ++++++++++++++++++- mm2src/coins/utxo/wallet_connect.rs | 49 ++++++++++ 4 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 mm2src/coins/utxo/wallet_connect.rs diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 265fc2ad12..ddaaf690e1 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -4220,12 +4220,12 @@ 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. - // FIXME: 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). + // 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, diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 53e9bc7cdb..d393b28272 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/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 64cfdfbe48..2da7c07433 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -5,6 +5,7 @@ use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientSettings, ElectrumC 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}; +use crate::utxo::wallet_connect::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, @@ -14,19 +15,26 @@ 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; +use kdf_walletconnect::chain::WcChainId; +use kdf_walletconnect::error::WalletConnectError; +use kdf_walletconnect::WalletConnectCtx; pub use keys::{Address, AddressBuilder, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressScriptType, KeyPair, Private, Public, Secret}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +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! { @@ -77,6 +85,7 @@ pub enum UtxoCoinBuildError { mode: String, }, InvalidPathToAddress(String), + WalletConnectError(String), } impl From for UtxoCoinBuildError { @@ -112,6 +121,10 @@ impl From for UtxoCoinBuildError { fn from(e: keys::Error) -> Self { UtxoCoinBuildError::Internal(e.to_string()) } } +impl From for UtxoCoinBuildError { + fn from(e: WalletConnectError) -> Self { UtxoCoinBuildError::WalletConnectError(e.to_string()) } +} + #[async_trait] pub trait UtxoCoinBuilder: UtxoCoinBuilderCommonOps { type ResultCoin; @@ -128,9 +141,8 @@ pub trait UtxoCoinBuilder: UtxoCoinBuilderCommonOps { build_utxo_fields_with_global_hd(self, global_hd_ctx).await }, PrivKeyBuildPolicy::Trezor => build_utxo_fields_with_trezor(self).await, - PrivKeyBuildPolicy::WalletConnect { .. } => { - // FIXME: don't panic. - panic!("for now"); + PrivKeyBuildPolicy::WalletConnect { session_topic } => { + build_utxo_fields_with_walletconnect(self, &session_topic).await }, } } @@ -224,6 +236,80 @@ where 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: &str, +) -> UtxoCoinBuildResult +where + Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, +{ + // Get and parse the chain_id of the coin. + let chain_id = builder.conf()["chain_id"].as_str().ok_or_else(|| { + UtxoCoinBuildError::WalletConnectError(format!( + "coin={} doesn't have chain_id (bip122 standard) set in coin config which is required for WalletConnect", + builder.ticker() + )) + })?; + let chain_id = WcChainId::try_from_str(chain_id)?; + + // Construct the coin config from the coin config and activation params. + let path_purpose_to_coin = builder.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 = builder.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:?}")) + })?; + + let wc_ctx = WalletConnectCtx::from_ctx(builder.ctx())?; + let (address, pubkey) = get_walletconnect_address(&wc_ctx, session_topic, &chain_id, &full_derivation_path).await?; + + // Construct the PrivKeyPolicy (of WalletConnect type). + let pubkey = PublicKey::from_str(&pubkey).map_err(|e| { + UtxoCoinBuildError::WalletConnectError(format!("Received a bad pubkey={} from WalletConnect: {}", pubkey, 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(), + }; + + let conf = UtxoConfBuilder::new(builder.conf(), builder.activation_params(), builder.ticker()).build()?; + + // 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 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(UtxoCoinBuildError::WalletConnectError(format!( + "Received address={} from WalletConnect doesn't match the expected address={}", + my_address_serialized, address + ))); + } + let derivation_method = DerivationMethod::SingleAddress(my_address); + + 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( builder: &Builder, conf: UtxoCoinConf, diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs new file mode 100644 index 0000000000..64f46ceb10 --- /dev/null +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -0,0 +1,49 @@ +//! This module provides functionality to interact with WalletConnect for UTXO-based coins. + +use crypto::StandardHDPath; +use kdf_walletconnect::{chain::{WcChainId, WcRequestMethods}, + error::WalletConnectError, + WalletConnectCtx}; +use mm2_err_handle::prelude::MmResult; + +/// This struct represents a UTXO address returned by GetAccountAddresses request in WalletConnect. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct UtxoAddress { + address: String, + public_key: String, + path: StandardHDPath, +} + +/// Get the enabled address (chosen by the user) +pub async fn get_walletconnect_address( + wc: &WalletConnectCtx, + session_topic: &str, + chain_id: &WcChainId, + derivation_path: &StandardHDPath, +) -> MmResult<(String, String), 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 + .into_iter() + .find(|a| a.path == *derivation_path) + .ok_or_else(|| { + WalletConnectError::NoAccountFound(format!( + "No address found for derivaiton path: {}", + derivation_path.to_string() + )) + })?; + Ok((account.address, account.public_key)) +} From f15163f537bba31df575f3503b9daa970f401ac6 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 21 Jun 2025 23:11:35 +0200 Subject: [PATCH 14/37] add bip122 to WcChainId struct --- mm2src/kdf_walletconnect/src/chain.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mm2src/kdf_walletconnect/src/chain.rs b/mm2src/kdf_walletconnect/src/chain.rs index 0b3c34bda7..44437427fe 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", } } } From 6dd49c17d57d541a03d60f53fda049f1197f6ab6 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Tue, 24 Jun 2025 11:10:45 +0200 Subject: [PATCH 15/37] fix ci clippy error --- mm2src/coins/utxo/wallet_connect.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index 64f46ceb10..bcbc7b9216 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -40,10 +40,7 @@ pub async fn get_walletconnect_address( .into_iter() .find(|a| a.path == *derivation_path) .ok_or_else(|| { - WalletConnectError::NoAccountFound(format!( - "No address found for derivaiton path: {}", - derivation_path.to_string() - )) + WalletConnectError::NoAccountFound(format!("No address found for derivaiton path: {}", derivation_path)) })?; Ok((account.address, account.public_key)) } From a665b5582d0b1ac7dc8e9d562f7cd2e07125940f Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 25 Jun 2025 09:10:19 +0200 Subject: [PATCH 16/37] strongtype wallet connect error --- .../utxo/utxo_builder/utxo_coin_builder.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 2da7c07433..98241c603d 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -85,7 +85,7 @@ pub enum UtxoCoinBuildError { mode: String, }, InvalidPathToAddress(String), - WalletConnectError(String), + WalletConnectError(WalletConnectError), } impl From for UtxoCoinBuildError { @@ -122,7 +122,7 @@ impl From for UtxoCoinBuildError { } impl From for UtxoCoinBuildError { - fn from(e: WalletConnectError) -> Self { UtxoCoinBuildError::WalletConnectError(e.to_string()) } + fn from(e: WalletConnectError) -> Self { UtxoCoinBuildError::WalletConnectError(e) } } #[async_trait] @@ -245,7 +245,7 @@ where { // Get and parse the chain_id of the coin. let chain_id = builder.conf()["chain_id"].as_str().ok_or_else(|| { - UtxoCoinBuildError::WalletConnectError(format!( + WalletConnectError::InvalidChainId(format!( "coin={} doesn't have chain_id (bip122 standard) set in coin config which is required for WalletConnect", builder.ticker() )) @@ -274,7 +274,7 @@ where // Construct the PrivKeyPolicy (of WalletConnect type). let pubkey = PublicKey::from_str(&pubkey).map_err(|e| { - UtxoCoinBuildError::WalletConnectError(format!("Received a bad pubkey={} from WalletConnect: {}", pubkey, e)) + WalletConnectError::ClientError(format!("Received a bad pubkey={} from WalletConnect: {}", pubkey, e)) })?; let public_key = pubkey.serialize().into(); let public_key_uncompressed = pubkey.serialize_uncompressed().into(); @@ -300,10 +300,13 @@ where .display_address() .map_err(|e| UtxoCoinBuildError::Internal(format!("Failed to serialize address: {}", e)))?; if my_address_serialized != address { - return MmError::err(UtxoCoinBuildError::WalletConnectError(format!( - "Received address={} from WalletConnect doesn't match the expected address={}", + return MmError::err( + WalletConnectError::ClientError(format!( + "Received address={} from WalletConnect doesn't match the expected address={} derived via the public key", my_address_serialized, address - ))); + )) + .into(), + ); } let derivation_method = DerivationMethod::SingleAddress(my_address); From 028ff49f362c8d6d69ca43a4bdde0c329139d5a4 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 25 Jun 2025 09:12:25 +0200 Subject: [PATCH 17/37] move a note to a todo to not forget about it later --- mm2src/kdf_walletconnect/src/chain.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/kdf_walletconnect/src/chain.rs b/mm2src/kdf_walletconnect/src/chain.rs index 44437427fe..0f731a23e0 100644 --- a/mm2src/kdf_walletconnect/src/chain.rs +++ b/mm2src/kdf_walletconnect/src/chain.rs @@ -94,7 +94,7 @@ pub enum WcRequestMethods { EthSignTransaction, EthSendTransaction, EthPersonalSign, - // Notes (remove later): + // 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) From b892571738f93f09f37e86c26a9687bfe663c8f4 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Mon, 30 Jun 2025 10:54:29 +0200 Subject: [PATCH 18/37] review(onur): rename the generic UtxoAddress struct and shorten its doc comment --- mm2src/coins/utxo/wallet_connect.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index bcbc7b9216..a305b232b0 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -6,10 +6,10 @@ use kdf_walletconnect::{chain::{WcChainId, WcRequestMethods}, WalletConnectCtx}; use mm2_err_handle::prelude::MmResult; -/// This struct represents a UTXO address returned by GetAccountAddresses request in WalletConnect. +/// Represents a UTXO address returned by GetAccountAddresses request in WalletConnect. #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct UtxoAddress { +struct GetAccountAddressesItem { address: String, public_key: String, path: StandardHDPath, @@ -27,7 +27,7 @@ pub async fn get_walletconnect_address( let params = json!({ "account": account_str, }); - let accounts: Vec = wc + let accounts: Vec = wc .send_session_request_and_wait( session_topic, chain_id, From 2c72ad65f9cb1a259ca11930774772b12aa8ec27 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Thu, 3 Jul 2025 09:57:36 +0200 Subject: [PATCH 19/37] turn the TryFrom back to From --- mm2src/coins/eth.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index acc4e924d8..4495fc759f 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -833,16 +833,14 @@ impl EthPrivKeyBuildPolicy { } } -impl TryFrom for EthPrivKeyBuildPolicy { - type Error = String; - - fn try_from(policy: PrivKeyBuildPolicy) -> Result { +impl From for EthPrivKeyBuildPolicy { + fn from(policy: PrivKeyBuildPolicy) -> EthPrivKeyBuildPolicy { match policy { - PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(iguana)), - PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd)), - PrivKeyBuildPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => EthPrivKeyBuildPolicy::IguanaPrivKey(iguana), + PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd), + PrivKeyBuildPolicy::Trezor => EthPrivKeyBuildPolicy::Trezor, PrivKeyBuildPolicy::WalletConnect { session_topic } => { - Ok(EthPrivKeyBuildPolicy::WalletConnect { session_topic }) + EthPrivKeyBuildPolicy::WalletConnect { session_topic } }, } } @@ -6877,8 +6875,7 @@ pub async fn get_eth_address( } else { PrivKeyBuildPolicy::detect_priv_key_policy(ctx).map_mm_err()? } - .try_into() - .map_err(GetEthAddressError::Internal)?; + .into(); let (_, derivation_method) = build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None, None) From 98f9d5f6db3d6516fc55d2071cd53caf48766a74 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Thu, 3 Jul 2025 10:17:11 +0200 Subject: [PATCH 20/37] manually convert PrivKeyBuildPoilcy to the ETH counterpart in legacy activation we used to convert it using `priv_key_policy.into()` and then do a check to rule out the bad activation policies not accepted by legacy activation. changing it now to just do the conversion inline and reject bad activation policies from the beginning. this way we don't accidently add a new activation policty but forget to rule it out in legacy activation --- mm2src/coins/eth.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 4495fc759f..85449f0120 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -6561,18 +6561,14 @@ pub async fn eth_coin_from_conf_and_request( } // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy`. - let priv_key_policy = try_s!(EthPrivKeyBuildPolicy::try_from(priv_key_policy)); - // Make sure not to allow new activation methods that are not supported for legacy ETH coin activation. - match priv_key_policy { - EthPrivKeyBuildPolicy::WalletConnect { .. } => { + 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"); }, - #[cfg(target_arch = "wasm32")] - EthPrivKeyBuildPolicy::Metamask { .. } => { - return ERR!("Metamask 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() { From 17bf308f88335bb112cef821eb9b59d40204763b Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 9 Jul 2025 17:58:53 +0200 Subject: [PATCH 21/37] refactor sign_message_hash for better usability --- mm2src/coins/qrc20.rs | 3 ++- mm2src/coins/utxo/bch.rs | 3 ++- mm2src/coins/utxo/qtum.rs | 3 ++- mm2src/coins/utxo/slp.rs | 3 ++- mm2src/coins/utxo/utxo_common.rs | 24 +++++++++++++++++------- mm2src/coins/utxo/utxo_standard.rs | 3 ++- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 702a2df165..76f819f158 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1042,7 +1042,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(utxo_common::sign_message_hash(prefix, message)) } fn sign_message(&self, message: &str, address: Option) -> SignatureResult { diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index f480ca6592..13a852eace 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1147,7 +1147,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(utxo_common::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 e41cbb7817..e64e89350f 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -767,7 +767,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(utxo_common::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 13ac99cbe0..1bfde4ac55 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -1133,7 +1133,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(utxo_common::sign_message_hash(prefix, message)) } fn sign_message(&self, message: &str, address: Option) -> SignatureResult { diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 31b4a9c8fc..854d5f0033 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2919,16 +2919,15 @@ pub fn burn_address(coin: &T) -> Result Option<[u8; 32]> { - let message_prefix = coin.conf.sign_message_prefix.clone()?; +pub fn sign_message_hash(sign_message_prefix: &str, message: &str) -> [u8; 32] { let mut stream = Stream::new(); - let prefix_len = CompactInteger::from(message_prefix.len()); + let prefix_len = CompactInteger::from(sign_message_prefix.len()); prefix_len.serialize(&mut stream); - stream.append_slice(message_prefix.as_bytes()); + 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()); - Some(dhash256(&stream.out()).take()) + dhash256(&stream.out()).take() } pub fn sign_message( @@ -2936,7 +2935,12 @@ pub fn sign_message( 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()?; @@ -2968,7 +2972,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 073b1b75f2..614d8248fc 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -857,7 +857,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(utxo_common::sign_message_hash(prefix, message)) } fn sign_message(&self, message: &str, address: Option) -> SignatureResult { From eee71ca14f3d655968159067041737b9bd761e3a Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 9 Jul 2025 18:26:40 +0200 Subject: [PATCH 22/37] impl msg signing and pubkey recovery with walletconnect yeah it works :D --- mm2src/coins/utxo/wallet_connect.rs | 73 ++++++++++++++++++++++++++- mm2src/kdf_walletconnect/src/chain.rs | 2 + 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index a305b232b0..0f8f9c46e6 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -1,17 +1,26 @@ //! This module provides functionality to interact with WalletConnect for UTXO-based coins. +use std::convert::TryFrom; +use crate::utxo::utxo_common; +use chain::hash::H256; use crypto::StandardHDPath; use kdf_walletconnect::{chain::{WcChainId, WcRequestMethods}, error::WalletConnectError, WalletConnectCtx}; -use mm2_err_handle::prelude::MmResult; +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, + // FIXME: this is optional public_key: String, + // FIXME: this is also optional path: StandardHDPath, } @@ -44,3 +53,65 @@ pub async fn get_walletconnect_address( })?; Ok((account.address, account.public_key)) } + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SignMessageResponse { + address: String, + signature: String, + #[allow(dead_code)] + message_hash: Option, +} + +pub async fn get_pubkey_via_wallatconnect_signature( + wc: &WalletConnectCtx, + session_topic: &str, + 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 = BASE64_ENGINE.decode(&signature_response.signature).map_err(|e| { + WalletConnectError::InternalError(format!( + "Failed to decode signature={}: {:?}", + signature_response.signature, e + )) + })?; + 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 = utxo_common::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/kdf_walletconnect/src/chain.rs b/mm2src/kdf_walletconnect/src/chain.rs index 0f731a23e0..ebf53c0e26 100644 --- a/mm2src/kdf_walletconnect/src/chain.rs +++ b/mm2src/kdf_walletconnect/src/chain.rs @@ -103,6 +103,7 @@ pub enum WcRequestMethods { UtxoGetAccountAddresses, UtxoSendTransfer, UtxoSignPsbt, + UtxoPersonalSign, } impl AsRef for WcRequestMethods { @@ -117,6 +118,7 @@ impl AsRef for WcRequestMethods { Self::UtxoGetAccountAddresses => "getAccountAddresses", Self::UtxoSendTransfer => "sendTransfer", Self::UtxoSignPsbt => "signPsbt", + Self::UtxoPersonalSign => "signMessage", } } } From 16aa6565d92f1ded77b1e19cb829b9b105c6b8a0 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 9 Jul 2025 19:15:38 +0200 Subject: [PATCH 23/37] use signMessage for pubkey recovery as a fallback in case the wallet doesn't make the pubkey available via getAccountAddresses --- .../utxo/utxo_builder/utxo_coin_builder.rs | 22 ++++++--- mm2src/coins/utxo/wallet_connect.rs | 45 ++++++++++++++----- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 2d42a1807e..dd76b56bd0 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -5,7 +5,7 @@ use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientSettings, ElectrumC 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}; -use crate::utxo::wallet_connect::get_walletconnect_address; +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, @@ -250,6 +250,10 @@ async fn build_utxo_fields_with_walletconnect( where Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, { + let conf = UtxoConfBuilder::new(builder.conf(), builder.activation_params(), builder.ticker()) + .build() + .map_mm_err()?; + // Get and parse the chain_id of the coin. let chain_id = builder.conf()["chain_id"].as_str().ok_or_else(|| { WalletConnectError::InvalidChainId(format!( @@ -281,6 +285,18 @@ where .await .map_mm_err()?; + let pubkey = match pubkey { + Some(pubkey) => pubkey, + 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()? + }, + }; + // Construct the PrivKeyPolicy (of WalletConnect type). let pubkey = PublicKey::from_str(&pubkey).map_err(|e| { WalletConnectError::ClientError(format!("Received a bad pubkey={} from WalletConnect: {}", pubkey, e)) @@ -293,10 +309,6 @@ where session_topic: session_topic.to_owned(), }; - let conf = UtxoConfBuilder::new(builder.conf(), builder.activation_params(), builder.ticker()) - .build() - .map_mm_err()?; - // Construct the derivation method (of SingleAddress type). let my_address = AddressBuilder::new( builder.address_format()?, diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index 0f8f9c46e6..0038c8f2c5 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -18,10 +18,10 @@ use base64::Engine; #[serde(rename_all = "camelCase")] struct GetAccountAddressesItem { address: String, - // FIXME: this is optional - public_key: String, - // FIXME: this is also optional - path: StandardHDPath, + public_key: Option, + path: Option, + #[allow(dead_code)] + intention: Option, } /// Get the enabled address (chosen by the user) @@ -30,7 +30,7 @@ pub async fn get_walletconnect_address( session_topic: &str, chain_id: &WcChainId, derivation_path: &StandardHDPath, -) -> MmResult<(String, String), WalletConnectError> { +) -> 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!({ @@ -44,16 +44,36 @@ pub async fn get_walletconnect_address( params, ) .await?; + // Find the address that the user is interested in (the enabled address). - let account = accounts - .into_iter() - .find(|a| a.path == *derivation_path) - .ok_or_else(|| { - WalletConnectError::NoAccountFound(format!("No address found for derivaiton path: {}", derivation_path)) - })?; - Ok((account.address, account.public_key)) + 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(format!( + "WalletConnect returned no addresses for getAccountAddresses" + )) + })?; + // 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 { @@ -63,6 +83,7 @@ struct SignMessageResponse { 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: &str, From f3451fe1b11bebd83177649afab8da2c10ece4df Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 9 Jul 2025 19:43:02 +0200 Subject: [PATCH 24/37] fix error in activated_key_or_err() call when walletconnect is used --- mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index dd76b56bd0..d03e3eccd5 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -345,7 +345,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, @@ -353,7 +358,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)?; From 4587a1d96a5d26415c861c0c91a0f7b46f534995 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 9 Jul 2025 19:47:47 +0200 Subject: [PATCH 25/37] fix linting issues --- mm2src/coins/utxo/wallet_connect.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index 0038c8f2c5..8d1466e714 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -54,9 +54,9 @@ pub async fn get_walletconnect_address( // 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(format!( - "WalletConnect returned no addresses for getAccountAddresses" - )) + 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() { From c86a2bf30687919b670dff87d77a1cce61f60aca Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Thu, 10 Jul 2025 10:34:49 +0200 Subject: [PATCH 26/37] review(onur): add more spaces here and there for readability --- .../utxo/utxo_builder/utxo_coin_builder.rs | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index b456873d39..053d8059b3 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -157,17 +157,19 @@ where let conf = UtxoConfBuilder::new(builder.conf(), builder.activation_params(), builder.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 = builder.address_format()?; + let my_address = AddressBuilder::new( - addr_format, + builder.address_format()?, conf.checksum_type, conf.address_prefixes.clone(), conf.bech32_hrp.clone(), @@ -175,6 +177,7 @@ where .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 } @@ -191,24 +194,27 @@ where .map_mm_err()?; let path_to_address = builder.activation_params().path_to_address; + let path_to_coin = conf .derivation_path .as_ref() - .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet) - .map_mm_err()?; + .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( - &path_to_address - .to_derivation_path(path_to_coin) - .mm_err(|e| UtxoCoinBuildError::InvalidPathToAddress(e.to_string()))?, - ) + .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 { @@ -223,10 +229,13 @@ where HDWalletCoinStorage::init_with_rmd160(builder.ctx(), builder.ticker().to_owned(), hd_wallet_rmd160) .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, @@ -238,6 +247,7 @@ where }, 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 } From 87bb907967b29c569a15aa14547e693d996cbc7d Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Mon, 14 Jul 2025 13:51:35 +0200 Subject: [PATCH 27/37] refine the todo regarding ledger connectino detection --- mm2src/kdf_walletconnect/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs index a5861f716f..ba00f1328f 100644 --- a/mm2src/kdf_walletconnect/src/lib.rs +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -493,7 +493,7 @@ impl WalletConnectCtxImpl { .get_session(&session_topic) .and_then(|session| session.session_properties) .and_then(|props| props.keys.as_ref().cloned()) - // TODO: Please re-check if this is correct. Why do we pick the first KeyInfo? shouldn't we pick the one with matching chain_id? + // 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) From 480ad3809eeaf1275d593621813f102b5be6cc42 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Mon, 14 Jul 2025 13:54:44 +0200 Subject: [PATCH 28/37] review(onur): rename build_..._iguana_secret to build_..._iguana_priv_key --- mm2src/coins/qrc20.rs | 4 ++-- mm2src/coins/utxo/utxo_builder/mod.rs | 2 +- mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 6e1b374b9e..23bd1e0d09 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -8,7 +8,7 @@ use crate::utxo::rpc_clients::{ElectrumClient, NativeClient, UnspentInfo, UtxoRp UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; #[cfg(not(target_arch = "wasm32"))] use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; -use crate::utxo::utxo_builder::{build_utxo_fields_with_global_hd, build_utxo_fields_with_iguana_secret, +use crate::utxo::utxo_builder::{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::{qtum, ActualFeeRate, AddrFromStrError, BroadcastTxErr, FeePolicy, GenerateTxError, GetUtxoListOps, @@ -273,7 +273,7 @@ impl UtxoCoinBuilder for Qrc20CoinBuilder<'_> { async fn build_utxo_fields(&self) -> UtxoCoinBuildResult { match self.priv_key_policy() { - PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => build_utxo_fields_with_iguana_secret(self, priv_key).await, + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => build_utxo_fields_with_iguana_priv_key(self, priv_key).await, PrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => { build_utxo_fields_with_global_hd(self, global_hd_ctx).await }, diff --git a/mm2src/coins/utxo/utxo_builder/mod.rs b/mm2src/coins/utxo/utxo_builder/mod.rs index bd84d08012..e9b7d666a4 100644 --- a/mm2src/coins/utxo/utxo_builder/mod.rs +++ b/mm2src/coins/utxo/utxo_builder/mod.rs @@ -3,7 +3,7 @@ mod utxo_coin_builder; mod utxo_conf_builder; pub use utxo_arc_builder::{MergeUtxoArcOps, UtxoArcBuilder}; -pub use utxo_coin_builder::{build_utxo_fields_with_global_hd, build_utxo_fields_with_iguana_secret, +pub use utxo_coin_builder::{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_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 053d8059b3..2cfe2f26d7 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -135,7 +135,7 @@ pub trait UtxoCoinBuilder: UtxoCoinBuilderCommonOps { async fn build_utxo_fields(&self) -> UtxoCoinBuildResult { match self.priv_key_policy() { - PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => build_utxo_fields_with_iguana_secret(self, priv_key).await, + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => build_utxo_fields_with_iguana_priv_key(self, priv_key).await, PrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => { build_utxo_fields_with_global_hd(self, global_hd_ctx).await }, @@ -147,7 +147,7 @@ pub trait UtxoCoinBuilder: UtxoCoinBuilderCommonOps { } } -pub async fn build_utxo_fields_with_iguana_secret( +pub async fn build_utxo_fields_with_iguana_priv_key( builder: &Builder, priv_key: IguanaPrivKey, ) -> UtxoCoinBuildResult From 9bf308ad4b63dd8ea9192d7fb5f9f59e3d8586b0 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Mon, 14 Jul 2025 15:32:11 +0200 Subject: [PATCH 29/37] review(onur): reduce the size of `build_utxo_fields_with_walletconnect` with helper funcs --- .../utxo/utxo_builder/utxo_coin_builder.rs | 66 +++++++++++-------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 2cfe2f26d7..9897f9cad4 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -263,31 +263,8 @@ where .build() .map_mm_err()?; - // Get and parse the chain_id of the coin. - let chain_id = builder.conf()["chain_id"].as_str().ok_or_else(|| { - WalletConnectError::InvalidChainId(format!( - "coin={} doesn't have chain_id (bip122 standard) set in coin config which is required for WalletConnect", - builder.ticker() - )) - })?; - let chain_id = WcChainId::try_from_str(chain_id).map_mm_err()?; - - // Construct the coin config from the coin config and activation params. - let path_purpose_to_coin = builder.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 = builder.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:?}")) - })?; + 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) @@ -296,6 +273,7 @@ where 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()) @@ -328,6 +306,11 @@ where .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)))?; @@ -340,7 +323,6 @@ where .into(), ); } - let derivation_method = DerivationMethod::SingleAddress(my_address); build_utxo_coin_fields_with_conf_and_policy(builder, conf, priv_key_policy, derivation_method).await } @@ -574,6 +556,38 @@ pub trait UtxoCoinBuilderCommonOps { Ok(BlockchainNetwork::Mainnet) } + /// Returns WcChainId for this coin. Parsed from the coin config. + fn wallet_connect_chain_id(&self) -> UtxoCoinBuildResult { + let chain_id = self.conf()["chain_id"].as_str().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(chain_id).map_mm_err()?; + Ok(chain_id) + } + + /// 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) } From 4149627b5cfb35e797168a82177aef37c6e1f820 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Mon, 14 Jul 2025 16:30:04 +0200 Subject: [PATCH 30/37] review(onur): use a specilizied struct for session_topic parameters (1/2) it's by no means a validated struct, but it makes it type-obvious what this parameter is (1/2): noticed that kdf_walletconnect ingests `&str` for the session_topic parameter, thus had to `session_topic.value()` a lot. will change this in the next commit. --- mm2src/coins/eth.rs | 2 +- mm2src/coins/eth/v2_activation.rs | 3 +- mm2src/coins/eth/wallet_connect.rs | 37 +++++++++++++------ mm2src/coins/lp_coins.rs | 6 +-- mm2src/coins/tendermint/tendermint_coin.rs | 6 +-- mm2src/coins/tendermint/wallet_connect.rs | 25 ++++++++----- .../utxo/utxo_builder/utxo_coin_builder.rs | 4 +- mm2src/coins/utxo/wallet_connect.rs | 25 ++++++++----- .../src/tendermint_with_assets_activation.rs | 8 ++-- mm2src/kdf_walletconnect/src/lib.rs | 5 ++- 10 files changed, 77 insertions(+), 44 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 78001bf5ba..29f1060211 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -821,7 +821,7 @@ pub enum EthPrivKeyBuildPolicy { Metamask(MetamaskArc), Trezor, WalletConnect { - session_topic: String, + session_topic: kdf_walletconnect::WcTopic, }, } diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 87325ee2ca..412f7f8235 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -15,6 +15,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}; @@ -177,7 +178,7 @@ pub enum EthPrivKeyActivationPolicy { #[cfg(target_arch = "wasm32")] Metamask, WalletConnect { - session_topic: String, + session_topic: WcTopic, }, } diff --git a/mm2src/coins/eth/wallet_connect.rs b/mm2src/coins/eth/wallet_connect.rs index 340f199125..bdb5c1aaa3 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}; @@ -113,7 +113,7 @@ impl WalletConnectOps for EthCoin { }, }; let chain_id = WcChainId::new_eip155(chain_id.to_string()); - wc.validate_update_active_chain_id(session_topic, &chain_id) + wc.validate_update_active_chain_id(session_topic.value(), &chain_id) .await .map_mm_err()?; @@ -130,7 +130,12 @@ impl WalletConnectOps for EthCoin { let tx_json = params.prepare_wc_tx_format()?; let session_topic = self.session_topic()?; let tx_hex: String = wc - .send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::EthSignTransaction, tx_json) + .send_session_request_and_wait( + session_topic.value(), + &chain_id, + WcRequestMethods::EthSignTransaction, + tx_json, + ) .await .map_mm_err()?; // if tx_hex.len() < 4 { @@ -159,9 +164,14 @@ impl WalletConnectOps for EthCoin { let chain_id = self.wc_chain_id(wc).await?; let tx_json = params.prepare_wc_tx_format()?; let session_topic = self.session_topic()?; - wc.send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::EthSendTransaction, tx_json) - .await - .map_mm_err()? + wc.send_session_request_and_wait( + session_topic.value(), + &chain_id, + WcRequestMethods::EthSendTransaction, + tx_json, + ) + .await + .map_mm_err()? }; let tx_hash = tx_hash.strip_prefix("0x").unwrap_or(&tx_hash); @@ -178,7 +188,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); } @@ -192,16 +202,16 @@ 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()); - wc.validate_update_active_chain_id(session_topic, &chain_id) + wc.validate_update_active_chain_id(session_topic.value(), &chain_id) .await .map_mm_err()?; let (account_str, _) = wc - .get_account_and_properties_for_chain_id(session_topic, &chain_id) + .get_account_and_properties_for_chain_id(session_topic.value(), &chain_id) .map_mm_err()?; let message = "Authenticate with KDF"; let params = { @@ -209,7 +219,12 @@ 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::EthPersonalSign, params) + .send_session_request_and_wait::( + session_topic.value(), + &chain_id, + WcRequestMethods::EthPersonalSign, + params, + ) .await .map_mm_err()?; diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 711f65bde1..f83d7585f2 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -4168,7 +4168,7 @@ pub enum PrivKeyActivationPolicy { ContextPrivKey, Trezor, WalletConnect { - session_topic: String, + session_topic: kdf_walletconnect::WcTopic, }, } @@ -4234,7 +4234,7 @@ pub enum PrivKeyPolicy { WalletConnect { public_key: H264, public_key_uncompressed: H520, - session_topic: String, + session_topic: kdf_walletconnect::WcTopic, }, } @@ -4373,7 +4373,7 @@ pub enum PrivKeyBuildPolicy { IguanaPrivKey(IguanaPrivKey), GlobalHDAccount(GlobalHDAccountArc), Trezor, - WalletConnect { session_topic: String }, + WalletConnect { session_topic: kdf_walletconnect::WcTopic }, } impl PrivKeyBuildPolicy { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index cb6b3b4ce5..a608ebd5c4 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -387,8 +387,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, @@ -1493,7 +1493,7 @@ impl TendermintCoin { let session_topic = self .session_topic() .expect("session_topic can't be None inside this block"); - let encode = |data| wc.encode(session_topic, data); + let encode = |data| wc.encode(session_topic.value(), data); json!({ "signerAddress": self.my_address()?, diff --git a/mm2src/coins/tendermint/wallet_connect.rs b/mm2src/coins/tendermint/wallet_connect.rs index d3242d4f77..74b905a707 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; @@ -83,7 +83,8 @@ impl WalletConnectOps for TendermintCoin { async fn wc_chain_id(&self, wc: &WalletConnectCtx) -> Result { let chain_id = WcChainId::new_cosmos(self.protocol_info.chain_id.to_string()); let session_topic = self.session_topic()?; - wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + wc.validate_update_active_chain_id(session_topic.value(), &chain_id) + .await?; Ok(chain_id) } @@ -94,13 +95,13 @@ impl WalletConnectOps for TendermintCoin { ) -> Result { let chain_id = self.wc_chain_id(wc).await?; let session_topic = self.session_topic()?; - let method = if wc.is_ledger_connection(session_topic) { + let method = if wc.is_ledger_connection(session_topic.value()) { WcRequestMethods::CosmosSignAmino } else { WcRequestMethods::CosmosSignDirect }; let data: CosmosTxSignedData = wc - .send_session_request_and_wait(session_topic, &chain_id, method, params) + .send_session_request_and_wait(session_topic.value(), &chain_id, method, params) .await?; let signature = general_purpose::STANDARD .decode(data.signature.signature) @@ -121,7 +122,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,13 +136,14 @@ 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()); - wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + wc.validate_update_active_chain_id(session_topic.value(), &chain_id) + .await?; - let (account, properties) = wc.get_account_and_properties_for_chain_id(session_topic, &chain_id)?; + let (account, properties) = wc.get_account_and_properties_for_chain_id(session_topic.value(), &chain_id)?; // Check if session has session_properties and return wallet account; if let Some(props) = properties { @@ -170,7 +172,12 @@ pub async fn cosmos_get_accounts_impl( let params = serde_json::to_value(&account).unwrap(); let accounts: Vec = wc - .send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::CosmosGetAccounts, params) + .send_session_request_and_wait( + session_topic.value(), + &chain_id, + WcRequestMethods::CosmosGetAccounts, + params, + ) .await?; accounts.first().cloned().or_mm_err(|| { diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 9897f9cad4..4b9dbea947 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -23,7 +23,7 @@ use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use kdf_walletconnect::chain::WcChainId; use kdf_walletconnect::error::WalletConnectError; -use kdf_walletconnect::WalletConnectCtx; +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::*; @@ -254,7 +254,7 @@ where async fn build_utxo_fields_with_walletconnect( builder: &Builder, - session_topic: &str, + session_topic: &WcTopic, ) -> UtxoCoinBuildResult where Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index 8d1466e714..81d3b19a98 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -6,7 +6,7 @@ use chain::hash::H256; use crypto::StandardHDPath; use kdf_walletconnect::{chain::{WcChainId, WcRequestMethods}, error::WalletConnectError, - WalletConnectCtx}; + WalletConnectCtx, WcTopic}; use keys::{CompactSignature, Public}; use mm2_err_handle::prelude::{MmError, MmResult}; @@ -27,18 +27,19 @@ struct GetAccountAddressesItem { /// Get the enabled address (chosen by the user) pub async fn get_walletconnect_address( wc: &WalletConnectCtx, - session_topic: &str, + 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)?; + wc.validate_update_active_chain_id(session_topic.value(), chain_id) + .await?; + let (account_str, _) = wc.get_account_and_properties_for_chain_id(session_topic.value(), chain_id)?; let params = json!({ "account": account_str, }); let accounts: Vec = wc .send_session_request_and_wait( - session_topic, + session_topic.value(), chain_id, WcRequestMethods::UtxoGetAccountAddresses, params, @@ -86,15 +87,16 @@ struct SignMessageResponse { /// Get the public key associated with some address via WalletConnect signature. pub async fn get_pubkey_via_wallatconnect_signature( wc: &WalletConnectCtx, - session_topic: &str, + 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)?; + wc.validate_update_active_chain_id(session_topic.value(), chain_id) + .await?; + let (account_str, _) = wc.get_account_and_properties_for_chain_id(session_topic.value(), chain_id)?; let params = json!({ "account": account_str, "address": address, @@ -102,7 +104,12 @@ pub async fn get_pubkey_via_wallatconnect_signature( "protocol": "ecdsa", }); let signature_response: SignMessageResponse = wc - .send_session_request_and_wait(session_topic, chain_id, WcRequestMethods::UtxoPersonalSign, params) + .send_session_request_and_wait( + session_topic.value(), + chain_id, + WcRequestMethods::UtxoPersonalSign, + params, + ) .await?; // The wallet is required to send back the same address in the response. diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 349fce564a..ac819202cb 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -19,7 +19,7 @@ use coins::tendermint::{cosmos_get_accounts_impl, tendermint_priv_key_policy, Co 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; @@ -50,7 +50,7 @@ pub enum TendermintPubkeyActivationParams { is_ledger_connection: bool, }, /// Activate with WalletConnect - WalletConnect { session_topic: String }, + WalletConnect { session_topic: WcTopic }, } #[derive(Clone, Deserialize)] @@ -223,7 +223,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> { @@ -234,7 +234,7 @@ async fn activate_with_walletconnect( ticker: ticker.to_string(), kind: TendermintInitErrorKind::UnableToFetchChainAccount(err.to_string()), })?; - let wallet_type = if wc.is_ledger_connection(&session_topic) { + let wallet_type = if wc.is_ledger_connection(session_topic.value()) { TendermintWalletConnectionType::WcLedger(session_topic) } else { TendermintWalletConnectionType::Wc(session_topic) diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs index ba00f1328f..85f58222b2 100644 --- a/mm2src/kdf_walletconnect/src/lib.rs +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -7,6 +7,9 @@ mod metadata; 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_session_proposal_request; use chain::{WcChainId, WcRequestMethods, SUPPORTED_PROTOCOL}; @@ -81,7 +84,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 From a22b14b530931355930dc0dea57da8d658109b36 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Mon, 14 Jul 2025 16:41:01 +0200 Subject: [PATCH 31/37] review(onur): let kdf_walletconnect-rust use &Topic instead of &str for library methods (2/2) --- mm2src/coins/eth/wallet_connect.rs | 31 +++++------------- mm2src/coins/tendermint/tendermint_coin.rs | 2 +- mm2src/coins/tendermint/wallet_connect.rs | 19 ++++------- mm2src/coins/utxo/wallet_connect.rs | 19 ++++------- .../src/tendermint_with_assets_activation.rs | 2 +- mm2src/kdf_walletconnect/src/lib.rs | 32 ++++++++----------- 6 files changed, 35 insertions(+), 70 deletions(-) diff --git a/mm2src/coins/eth/wallet_connect.rs b/mm2src/coins/eth/wallet_connect.rs index bdb5c1aaa3..a541d818a8 100644 --- a/mm2src/coins/eth/wallet_connect.rs +++ b/mm2src/coins/eth/wallet_connect.rs @@ -113,7 +113,7 @@ impl WalletConnectOps for EthCoin { }, }; let chain_id = WcChainId::new_eip155(chain_id.to_string()); - wc.validate_update_active_chain_id(session_topic.value(), &chain_id) + wc.validate_update_active_chain_id(session_topic, &chain_id) .await .map_mm_err()?; @@ -130,12 +130,7 @@ impl WalletConnectOps for EthCoin { let tx_json = params.prepare_wc_tx_format()?; let session_topic = self.session_topic()?; let tx_hex: String = wc - .send_session_request_and_wait( - session_topic.value(), - &chain_id, - WcRequestMethods::EthSignTransaction, - tx_json, - ) + .send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::EthSignTransaction, tx_json) .await .map_mm_err()?; // if tx_hex.len() < 4 { @@ -164,14 +159,9 @@ impl WalletConnectOps for EthCoin { let chain_id = self.wc_chain_id(wc).await?; let tx_json = params.prepare_wc_tx_format()?; let session_topic = self.session_topic()?; - wc.send_session_request_and_wait( - session_topic.value(), - &chain_id, - WcRequestMethods::EthSendTransaction, - tx_json, - ) - .await - .map_mm_err()? + wc.send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::EthSendTransaction, tx_json) + .await + .map_mm_err()? }; let tx_hash = tx_hash.strip_prefix("0x").unwrap_or(&tx_hash); @@ -206,12 +196,12 @@ pub async fn eth_request_wc_personal_sign( chain_id: u64, ) -> MmResult<(H520, Address), EthWalletConnectError> { let chain_id = WcChainId::new_eip155(chain_id.to_string()); - wc.validate_update_active_chain_id(session_topic.value(), &chain_id) + wc.validate_update_active_chain_id(session_topic, &chain_id) .await .map_mm_err()?; let (account_str, _) = wc - .get_account_and_properties_for_chain_id(session_topic.value(), &chain_id) + .get_account_and_properties_for_chain_id(session_topic, &chain_id) .map_mm_err()?; let message = "Authenticate with KDF"; let params = { @@ -219,12 +209,7 @@ pub async fn eth_request_wc_personal_sign( json!(&[&message_hex, &account_str]) }; let data = wc - .send_session_request_and_wait::( - session_topic.value(), - &chain_id, - WcRequestMethods::EthPersonalSign, - params, - ) + .send_session_request_and_wait::(session_topic, &chain_id, WcRequestMethods::EthPersonalSign, params) .await .map_mm_err()?; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index a608ebd5c4..3fa3205fe8 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -1493,7 +1493,7 @@ impl TendermintCoin { let session_topic = self .session_topic() .expect("session_topic can't be None inside this block"); - let encode = |data| wc.encode(session_topic.value(), data); + let encode = |data| wc.encode(session_topic, data); json!({ "signerAddress": self.my_address()?, diff --git a/mm2src/coins/tendermint/wallet_connect.rs b/mm2src/coins/tendermint/wallet_connect.rs index 74b905a707..e8df678ce9 100644 --- a/mm2src/coins/tendermint/wallet_connect.rs +++ b/mm2src/coins/tendermint/wallet_connect.rs @@ -83,8 +83,7 @@ impl WalletConnectOps for TendermintCoin { async fn wc_chain_id(&self, wc: &WalletConnectCtx) -> Result { let chain_id = WcChainId::new_cosmos(self.protocol_info.chain_id.to_string()); let session_topic = self.session_topic()?; - wc.validate_update_active_chain_id(session_topic.value(), &chain_id) - .await?; + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; Ok(chain_id) } @@ -95,13 +94,13 @@ impl WalletConnectOps for TendermintCoin { ) -> Result { let chain_id = self.wc_chain_id(wc).await?; let session_topic = self.session_topic()?; - let method = if wc.is_ledger_connection(session_topic.value()) { + let method = if wc.is_ledger_connection(session_topic) { WcRequestMethods::CosmosSignAmino } else { WcRequestMethods::CosmosSignDirect }; let data: CosmosTxSignedData = wc - .send_session_request_and_wait(session_topic.value(), &chain_id, method, params) + .send_session_request_and_wait(session_topic, &chain_id, method, params) .await?; let signature = general_purpose::STANDARD .decode(data.signature.signature) @@ -140,10 +139,9 @@ pub async fn cosmos_get_accounts_impl( chain_id: &str, ) -> MmResult { let chain_id = WcChainId::new_cosmos(chain_id.to_string()); - wc.validate_update_active_chain_id(session_topic.value(), &chain_id) - .await?; + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; - let (account, properties) = wc.get_account_and_properties_for_chain_id(session_topic.value(), &chain_id)?; + let (account, properties) = wc.get_account_and_properties_for_chain_id(session_topic, &chain_id)?; // Check if session has session_properties and return wallet account; if let Some(props) = properties { @@ -172,12 +170,7 @@ pub async fn cosmos_get_accounts_impl( let params = serde_json::to_value(&account).unwrap(); let accounts: Vec = wc - .send_session_request_and_wait( - session_topic.value(), - &chain_id, - WcRequestMethods::CosmosGetAccounts, - params, - ) + .send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::CosmosGetAccounts, params) .await?; accounts.first().cloned().or_mm_err(|| { diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index 81d3b19a98..38ed155247 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -31,15 +31,14 @@ pub async fn get_walletconnect_address( chain_id: &WcChainId, derivation_path: &StandardHDPath, ) -> MmResult<(String, Option), WalletConnectError> { - wc.validate_update_active_chain_id(session_topic.value(), chain_id) - .await?; - let (account_str, _) = wc.get_account_and_properties_for_chain_id(session_topic.value(), chain_id)?; + 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.value(), + session_topic, chain_id, WcRequestMethods::UtxoGetAccountAddresses, params, @@ -94,9 +93,8 @@ pub async fn get_pubkey_via_wallatconnect_signature( ) -> MmResult { const AUTH_MSG: &str = "Authenticate with KDF"; - wc.validate_update_active_chain_id(session_topic.value(), chain_id) - .await?; - let (account_str, _) = wc.get_account_and_properties_for_chain_id(session_topic.value(), chain_id)?; + 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, @@ -104,12 +102,7 @@ pub async fn get_pubkey_via_wallatconnect_signature( "protocol": "ecdsa", }); let signature_response: SignMessageResponse = wc - .send_session_request_and_wait( - session_topic.value(), - chain_id, - WcRequestMethods::UtxoPersonalSign, - params, - ) + .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. diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index ac819202cb..454e438bcb 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -234,7 +234,7 @@ async fn activate_with_walletconnect( ticker: ticker.to_string(), kind: TendermintInitErrorKind::UnableToFetchChainAccount(err.to_string()), })?; - let wallet_type = if wc.is_ledger_connection(session_topic.value()) { + let wallet_type = if wc.is_ledger_connection(&session_topic) { TendermintWalletConnectionType::WcLedger(session_topic) } else { TendermintWalletConnectionType::Wc(session_topic) diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs index 85f58222b2..a1b77d7a66 100644 --- a/mm2src/kdf_walletconnect/src/lib.rs +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -389,11 +389,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); @@ -490,10 +489,9 @@ 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 @@ -504,10 +502,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() } @@ -549,13 +546,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(), )))?; @@ -609,13 +605,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(), )))?; @@ -637,7 +632,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, @@ -645,8 +640,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(), @@ -657,7 +651,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 From f17f300d4866b44253ee10aa46027df49a8f9a7b Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Mon, 28 Jul 2025 22:42:32 +0200 Subject: [PATCH 32/37] try to decode the signature response as hex first per the spec, the siganture should be in hex format. we fallback to base64 though since bifrost wrongfully uses base64 for signature encoding. ref. https://docs.reown.com/advanced/multichain/rpc-reference/bitcoin-rpc\#returns-4 --- mm2src/coins/utxo/wallet_connect.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs index 007c85d6cb..d8c1688897 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -116,12 +116,17 @@ pub async fn get_pubkey_via_wallatconnect_signature( ))); } - let decoded_signature = BASE64_ENGINE.decode(&signature_response.signature).map_err(|e| { - WalletConnectError::InternalError(format!( - "Failed to decode signature={}: {:?}", - signature_response.signature, e - )) - })?; + 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: {:?}", From 65212468416c1576e9a198250254364410a756f8 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Tue, 29 Jul 2025 13:53:08 +0200 Subject: [PATCH 33/37] move sign_message_hash to mm2_bitcoin --- mm2src/coins/qrc20.rs | 4 ++-- mm2src/coins/utxo/bch.rs | 3 ++- mm2src/coins/utxo/qtum.rs | 3 ++- mm2src/coins/utxo/slp.rs | 4 ++-- mm2src/coins/utxo/utxo_common.rs | 20 ++------------------ mm2src/coins/utxo/utxo_standard.rs | 3 ++- mm2src/coins/utxo/wallet_connect.rs | 4 ++-- mm2src/mm2_bitcoin/crypto/src/lib.rs | 14 ++++++++++++++ 8 files changed, 28 insertions(+), 27 deletions(-) diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index b926f84b8c..dbd2a96ebf 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -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}; @@ -1106,7 +1106,7 @@ impl MarketCoinOps for Qrc20Coin { fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?; - Some(utxo_common::sign_message_hash(prefix, message)) + Some(sign_message_hash(prefix, message)) } fn sign_message(&self, message: &str, address: Option) -> SignatureResult { diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 8c98b39e89..c33cdede94 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; @@ -1188,7 +1189,7 @@ impl MarketCoinOps for BchCoin { fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?; - Some(utxo_common::sign_message_hash(prefix, message)) + 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 68979889d8..b8c5eedca6 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -41,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}; @@ -815,7 +816,7 @@ impl MarketCoinOps for QtumCoin { fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?; - Some(utxo_common::sign_message_hash(prefix, message)) + 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 df28c268cb..87cd3fde56 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}; @@ -1203,7 +1203,7 @@ impl MarketCoinOps for SlpToken { fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?; - Some(utxo_common::sign_message_hash(prefix, message)) + Some(sign_message_hash(prefix, message)) } fn sign_message(&self, message: &str, address: Option) -> SignatureResult { diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 0a7a2d0dd8..86015b02c5 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; @@ -2942,19 +2939,6 @@ pub fn burn_address(coin: &T) -> Result [u8; 32] { - 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() -} - pub fn sign_message( coin: &UtxoCoinFields, message: &str, diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 6b0e6daf78..85a4b17820 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; @@ -889,7 +890,7 @@ impl MarketCoinOps for UtxoStandardCoin { fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?; - Some(utxo_common::sign_message_hash(prefix, message)) + 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 index d8c1688897..5d4f750959 100644 --- a/mm2src/coins/utxo/wallet_connect.rs +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -1,7 +1,7 @@ //! This module provides functionality to interact with WalletConnect for UTXO-based coins. use std::convert::TryFrom; -use crate::utxo::utxo_common; +use bitcrypto::sign_message_hash; use chain::hash::H256; use crypto::StandardHDPath; use kdf_walletconnect::{ @@ -133,7 +133,7 @@ pub async fn get_pubkey_via_wallatconnect_signature( signature_response.signature, e )) })?; - let message_hash = utxo_common::sign_message_hash(sign_message_prefix, AUTH_MSG); + 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={:?}: {:?}", 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}; From 84f3f0b494ba26efba4f32f59574b94837891c0f Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 30 Jul 2025 11:47:48 +0200 Subject: [PATCH 34/37] review(shamardy): report the correct error when tron is used with walletconnect --- mm2src/coins/eth/v2_activation.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 1e4cda1c29..8bab5c09c3 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -884,10 +884,11 @@ pub(crate) async fn build_address_and_priv_key_policy( let wc = WalletConnectCtx::from_ctx(ctx).map_err(|e| { EthActivationV2Error::WalletConnectError(format!("Failed to get WalletConnect context: {}", e)) })?; - let chain_id = chain_spec - .ok_or(EthActivationV2Error::ChainIdNotSet)? - .chain_id() - .ok_or(EthActivationV2Error::ChainIdNotSet)?; + 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()))?; From e893297b1e025e0399a73b17e0a24aa27d8eec48 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 30 Jul 2025 13:09:37 +0200 Subject: [PATCH 35/37] review(shamardy): move chain_id inside protocol data for utxo this is consistent with eth --- mm2src/coins/hd_wallet/pubkey.rs | 2 +- mm2src/coins/lp_coins.rs | 15 ++++++--- .../coins/rpc_command/init_create_account.rs | 5 ++- .../utxo/utxo_builder/utxo_coin_builder.rs | 31 +++++++++++++------ .../utxo/utxo_builder/utxo_conf_builder.rs | 1 + .../src/utxo_activation/common_impl.rs | 5 ++- .../init_utxo_standard_activation.rs | 2 +- mm2src/mm2_main/src/lp_ordermatch.rs | 2 +- 8 files changed, 44 insertions(+), 19 deletions(-) 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 9ec2e4bb65..161aaabc49 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -4733,7 +4733,12 @@ pub trait IguanaBalanceOps { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "protocol_data")] pub enum CoinProtocol { - UTXO, + UTXO { + /// 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: Option, + }, QTUM, QRC20 { platform: String, @@ -4816,7 +4821,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 { .. } @@ -4835,7 +4840,7 @@ impl CoinProtocol { Some(contract_address) }, CoinProtocol::SLPTOKEN { .. } - | CoinProtocol::UTXO + | CoinProtocol::UTXO { .. } | CoinProtocol::QTUM | CoinProtocol::ETH { .. } | CoinProtocol::TRX { .. } @@ -5127,7 +5132,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() }, @@ -5800,7 +5805,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/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index 293aea246e..a0de01e7e6 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -310,7 +310,10 @@ impl RpcTask for InitCreateAccountTask { self.task_state.clone(), task_handle, utxo.is_trezor(), - CoinProtocol::UTXO, + CoinProtocol::UTXO { + // Note that the actual chain_id isn't needed by trezor XPUB extractor. + chain_id: None, + }, ) .await?, )), diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index d721da5397..f1f78490b1 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -14,8 +14,9 @@ use crate::utxo::{ 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; @@ -586,14 +587,26 @@ pub trait UtxoCoinBuilderCommonOps { /// Returns WcChainId for this coin. Parsed from the coin config. fn wallet_connect_chain_id(&self) -> UtxoCoinBuildResult { - let chain_id = self.conf()["chain_id"].as_str().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 protocol: CoinProtocol = json::from_value(self.conf()["protocol"].clone()).map_to_mm(|e| { + UtxoCoinBuildError::ConfError(UtxoConfError::InvalidProtocolData(format!( + "Couldn't parse protocol info: {e}" + ))) })?; - let chain_id = WcChainId::try_from_str(chain_id).map_mm_err()?; - Ok(chain_id) + + if let CoinProtocol::UTXO { chain_id } = protocol { + let chain_id = chain_id.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(&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. diff --git a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs index d6cf68d7a9..8ba20feb17 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_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index 4ef1ddedba..33d22921ec 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -49,7 +49,10 @@ where ctx, task_handle.clone(), xpub_extractor_rpc_statuses(), - coins::CoinProtocol::UTXO, + coins::CoinProtocol::UTXO { + // Note that the actual chain_id isn't needed by trezor XPUB extractor. + chain_id: None, + }, ) .mm_err(|_| InitUtxoStandardError::HwError(HwRpcError::NotInitialized))?, ) 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 4ca61ccc10..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), } } diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 0b78c3dc40..49443b7e58 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -6346,7 +6346,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) From 821a5dd44969df4b331092c2144fc1c04058ef89 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 30 Jul 2025 17:35:37 +0200 Subject: [PATCH 36/37] fix failing tests looks like serde won't regonize an object that has no protocol_data when the enum content is inlined as struct representation in the enum. to curcumvent this without forcing us to use protocol_data everywhere, we let the enum hold its content in a tuple representation and let that tuple hold a struct that has the needed content. --- mm2src/coins/lp_coins.rs | 16 ++++++++++------ mm2src/coins/rpc_command/init_create_account.rs | 6 ++---- .../coins/utxo/utxo_builder/utxo_coin_builder.rs | 4 ++-- .../src/utxo_activation/common_impl.rs | 6 ++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 61b68c2e1d..ebcf599128 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -4687,16 +4687,20 @@ 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: Option, +} + #[allow(clippy::upper_case_acronyms)] #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "protocol_data")] pub enum CoinProtocol { - UTXO { - /// 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: Option, - }, + UTXO(UtxoProtocolInfo), QTUM, QRC20 { platform: String, diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index 73e914a906..c77b38344f 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -310,10 +310,8 @@ impl RpcTask for InitCreateAccountTask { self.task_state.clone(), task_handle, utxo.is_trezor(), - CoinProtocol::UTXO { - // Note that the actual chain_id isn't needed by trezor XPUB extractor. - chain_id: None, - }, + // Note that the actual UtxoProtocolInfo isn't needed by trezor XPUB extractor. + CoinProtocol::UTXO(Default::default()), ) .await?, )), diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 382c587e55..de5ceb38c9 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -16,7 +16,7 @@ use crate::utxo::{ use crate::{ BlockchainNetwork, CoinProtocol, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, SharableRpcTransportEventHandler, - UtxoActivationParams, + UtxoActivationParams, UtxoProtocolInfo, }; use async_trait::async_trait; use chain::TxHashAlgo; @@ -591,7 +591,7 @@ pub trait UtxoCoinBuilderCommonOps { ))) })?; - if let CoinProtocol::UTXO { chain_id } = protocol { + if let CoinProtocol::UTXO(UtxoProtocolInfo { chain_id }) = protocol { let chain_id = chain_id.ok_or_else(|| { WalletConnectError::InvalidChainId(format!( "coin={} doesn't have chain_id (bip122 standard) set in coin config which is required for WalletConnect", diff --git a/mm2src/coins_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index 33d22921ec..ed98fe4b3f 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -49,10 +49,8 @@ where ctx, task_handle.clone(), xpub_extractor_rpc_statuses(), - coins::CoinProtocol::UTXO { - // Note that the actual chain_id isn't needed by trezor XPUB extractor. - chain_id: None, - }, + // Note that the actual UtxoProtocolInfo isn't needed by trezor XPUB extractor. + coins::CoinProtocol::UTXO(Default::default()), ) .mm_err(|_| InitUtxoStandardError::HwError(HwRpcError::NotInitialized))?, ) From 188424ae139e78081d2bf88e25d157cb81bfe77d Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 30 Jul 2025 18:09:46 +0200 Subject: [PATCH 37/37] actually fix tests the optional tagging should be at the enum level. this is how it masks protocol_data to make it optional as well. structurally, this doesn't look right. but it keeps backward compatability with coins file and only coins that add chain_id need to play with protocol_data field. --- mm2src/coins/lp_coins.rs | 5 +++-- mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index ebcf599128..e491635700 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -4693,14 +4693,15 @@ 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: Option, + chain_id: String, } #[allow(clippy::upper_case_acronyms)] #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "protocol_data")] pub enum CoinProtocol { - UTXO(UtxoProtocolInfo), + // 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, diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index de5ceb38c9..d81246d881 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -16,7 +16,7 @@ use crate::utxo::{ use crate::{ BlockchainNetwork, CoinProtocol, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, SharableRpcTransportEventHandler, - UtxoActivationParams, UtxoProtocolInfo, + UtxoActivationParams, }; use async_trait::async_trait; use chain::TxHashAlgo; @@ -591,14 +591,14 @@ pub trait UtxoCoinBuilderCommonOps { ))) })?; - if let CoinProtocol::UTXO(UtxoProtocolInfo { chain_id }) = protocol { - let chain_id = chain_id.ok_or_else(|| { + 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(&chain_id).map_mm_err()?; + let chain_id = WcChainId::try_from_str(&utxo_info.chain_id).map_mm_err()?; Ok(chain_id) } else { MmError::err(UtxoCoinBuildError::ConfError(UtxoConfError::InvalidProtocolData(