Skip to content

Commit db98665

Browse files
mariocynicysdimxy
authored andcommitted
feat(wallet-connect): impl BTC (UTxO) activation via WalletConnect (#2499)
This implements activation only, full WalletConnect implementation for UTXO coins will follow.
1 parent 7a8f6f2 commit db98665

35 files changed

+704
-381
lines changed

mm2src/coins/eth.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ use web3::{self, Web3};
120120

121121
cfg_wasm32! {
122122
use crypto::MetamaskArc;
123-
use ethereum_types::H520;
124123
use mm2_metamask::MetamaskError;
125124
use web3::types::TransactionRequest;
126125
}
@@ -866,9 +865,7 @@ pub enum EthPrivKeyBuildPolicy {
866865
Metamask(MetamaskArc),
867866
Trezor,
868867
WalletConnect {
869-
address: Address,
870-
public_key_uncompressed: H520,
871-
session_topic: String,
868+
session_topic: kdf_walletconnect::WcTopic,
872869
},
873870
}
874871

@@ -889,11 +886,14 @@ impl EthPrivKeyBuildPolicy {
889886
}
890887

891888
impl From<PrivKeyBuildPolicy> for EthPrivKeyBuildPolicy {
892-
fn from(policy: PrivKeyBuildPolicy) -> Self {
889+
fn from(policy: PrivKeyBuildPolicy) -> EthPrivKeyBuildPolicy {
893890
match policy {
894891
PrivKeyBuildPolicy::IguanaPrivKey(iguana) => EthPrivKeyBuildPolicy::IguanaPrivKey(iguana),
895892
PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd),
896893
PrivKeyBuildPolicy::Trezor => EthPrivKeyBuildPolicy::Trezor,
894+
PrivKeyBuildPolicy::WalletConnect { session_topic } => {
895+
EthPrivKeyBuildPolicy::WalletConnect { session_topic }
896+
},
897897
}
898898
}
899899
}
@@ -2992,8 +2992,7 @@ async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> Raw
29922992
.map_to_mm(|err| RawTransactionError::TransactionError(err.get_plain_text_format()))
29932993
},
29942994
EthPrivKeyPolicy::WalletConnect { .. } => {
2995-
// NOTE: doesn't work with wallets that doesn't support `eth_signTransaction`.
2996-
// e.g Metamask
2995+
// NOTE: doesn't work with wallets that doesn't support `eth_signTransaction`. e.g TrustWallet
29972996
let wc = {
29982997
let ctx = MmArc::from_weak(&coin.ctx).expect("No context");
29992998
WalletConnectCtx::from_ctx(&ctx)
@@ -6564,8 +6563,15 @@ pub async fn eth_coin_from_conf_and_request(
65646563
}
65656564
}
65666565

6567-
// Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible.
6568-
let priv_key_policy = From::from(priv_key_policy);
6566+
// Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy`.
6567+
let priv_key_policy = match priv_key_policy {
6568+
PrivKeyBuildPolicy::IguanaPrivKey(iguana) => EthPrivKeyBuildPolicy::IguanaPrivKey(iguana),
6569+
PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd),
6570+
PrivKeyBuildPolicy::Trezor => EthPrivKeyBuildPolicy::Trezor,
6571+
PrivKeyBuildPolicy::WalletConnect { .. } => {
6572+
return ERR!("WalletConnect private key policy is not supported for legacy ETH coin activation");
6573+
},
6574+
};
65696575

65706576
let mut urls: Vec<String> = try_s!(json::from_value(req["urls"].clone()));
65716577
if urls.is_empty() {
@@ -6590,8 +6596,9 @@ pub async fn eth_coin_from_conf_and_request(
65906596
req["path_to_address"].clone()
65916597
))
65926598
.unwrap_or_default();
6593-
let (key_pair, derivation_method) =
6594-
try_s!(build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, &path_to_address, None).await);
6599+
let (key_pair, derivation_method) = try_s!(
6600+
build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, &path_to_address, None, None).await
6601+
);
65956602

65966603
let mut web3_instances = vec![];
65976604
let event_handlers = rpc_event_handlers_for_eth_transport(ctx, ticker.to_string());
@@ -6869,7 +6876,7 @@ pub async fn get_eth_address(
68696876
.into();
68706877

68716878
let (_, derivation_method) =
6872-
build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None)
6879+
build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None, None)
68736880
.await
68746881
.map_mm_err()?;
68756882
let my_address = derivation_method.single_addr_or_err().await.map_mm_err()?;

mm2src/coins/eth/v2_activation.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::*;
22
use crate::eth::erc20::{get_enabled_erc20_by_platform_and_contract, get_token_decimals};
3+
use crate::eth::wallet_connect::eth_request_wc_personal_sign;
34
use crate::eth::web3_transport::http_transport::HttpTransport;
45
use crate::hd_wallet::{
56
load_hd_accounts_from_storage, HDAccountsMutex, HDPathAccountToAddressId, HDWalletCoinStorage,
@@ -17,6 +18,7 @@ use crypto::{trezor::TrezorError, Bip32Error, CryptoCtxError, HwError};
1718
use enum_derives::EnumFromTrait;
1819
use ethereum_types::H264;
1920
use kdf_walletconnect::error::WalletConnectError;
21+
use kdf_walletconnect::WcTopic;
2022
use mm2_err_handle::common_errors::WithInternal;
2123
#[cfg(target_arch = "wasm32")]
2224
use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError};
@@ -201,7 +203,7 @@ pub enum EthPrivKeyActivationPolicy {
201203
#[cfg(target_arch = "wasm32")]
202204
Metamask,
203205
WalletConnect {
204-
session_topic: String,
206+
session_topic: WcTopic,
205207
},
206208
}
207209

@@ -681,6 +683,7 @@ pub async fn eth_coin_from_conf_and_request_v2(
681683
priv_key_build_policy,
682684
&req.path_to_address,
683685
req.gap_limit,
686+
Some(&chain_spec),
684687
)
685688
.await?;
686689

@@ -781,6 +784,7 @@ pub(crate) async fn build_address_and_priv_key_policy(
781784
priv_key_build_policy: EthPrivKeyBuildPolicy,
782785
path_to_address: &HDPathAccountToAddressId,
783786
gap_limit: Option<u32>,
787+
chain_spec: Option<&ChainSpec>,
784788
) -> MmResult<(EthPrivKeyPolicy, EthDerivationMethod), EthActivationV2Error> {
785789
match priv_key_build_policy {
786790
EthPrivKeyBuildPolicy::IguanaPrivKey(iguana) => {
@@ -875,11 +879,18 @@ pub(crate) async fn build_address_and_priv_key_policy(
875879
DerivationMethod::SingleAddress(address),
876880
))
877881
},
878-
EthPrivKeyBuildPolicy::WalletConnect {
879-
address,
880-
public_key_uncompressed,
881-
session_topic,
882-
} => {
882+
EthPrivKeyBuildPolicy::WalletConnect { session_topic } => {
883+
let wc = WalletConnectCtx::from_ctx(ctx).map_err(|e| {
884+
EthActivationV2Error::WalletConnectError(format!("Failed to get WalletConnect context: {e}"))
885+
})?;
886+
let chain_spec = chain_spec.ok_or(EthActivationV2Error::ChainIdNotSet)?;
887+
let chain_id = chain_spec.chain_id().ok_or(EthActivationV2Error::UnsupportedChain {
888+
chain: chain_spec.kind().to_string(),
889+
feature: "WalletConnect".to_string(),
890+
})?;
891+
let (public_key_uncompressed, address) = eth_request_wc_personal_sign(&wc, &session_topic, chain_id)
892+
.await
893+
.mm_err(|err| EthActivationV2Error::WalletConnectError(err.to_string()))?;
883894
let public_key = compress_public_key(public_key_uncompressed)?;
884895
Ok((
885896
EthPrivKeyPolicy::WalletConnect {

mm2src/coins/eth/wallet_connect.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use ethereum_types::{Address, Public, H160, H520, U256};
1616
use ethkey::{public_to_address, Message, Signature};
1717
use kdf_walletconnect::chain::{WcChainId, WcRequestMethods};
1818
use kdf_walletconnect::error::WalletConnectError;
19-
use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps};
19+
use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps, WcTopic};
2020
use mm2_err_handle::prelude::*;
2121
use secp256k1::recovery::{RecoverableSignature, RecoveryId};
2222
use secp256k1::{PublicKey, Secp256k1};
@@ -180,7 +180,7 @@ impl WalletConnectOps for EthCoin {
180180
Ok((signed_tx, tx_hex))
181181
}
182182

183-
fn session_topic(&self) -> Result<&str, Self::Error> {
183+
fn session_topic(&self) -> Result<&WcTopic, Self::Error> {
184184
if let EthPrivKeyPolicy::WalletConnect { ref session_topic, .. } = &self.priv_key_policy {
185185
return Ok(session_topic);
186186
}
@@ -194,7 +194,7 @@ impl WalletConnectOps for EthCoin {
194194

195195
pub async fn eth_request_wc_personal_sign(
196196
wc: &WalletConnectCtx,
197-
session_topic: &str,
197+
session_topic: &WcTopic,
198198
chain_id: u64,
199199
) -> MmResult<(H520, Address), EthWalletConnectError> {
200200
let chain_id = WcChainId::new_eip155(chain_id.to_string());
@@ -211,7 +211,7 @@ pub async fn eth_request_wc_personal_sign(
211211
json!(&[&message_hex, &account_str])
212212
};
213213
let data = wc
214-
.send_session_request_and_wait::<String>(session_topic, &chain_id, WcRequestMethods::PersonalSign, params)
214+
.send_session_request_and_wait::<String>(session_topic, &chain_id, WcRequestMethods::EthPersonalSign, params)
215215
.await
216216
.map_mm_err()?;
217217

mm2src/coins/hd_wallet/pubkey.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ where
136136
.or_mm_err(|| HDExtractPubkeyError::HwContextNotInitialized)?;
137137

138138
let trezor_message_type = match coin_protocol {
139-
CoinProtocol::UTXO => TrezorMessageType::Bitcoin,
139+
CoinProtocol::UTXO { .. } => TrezorMessageType::Bitcoin,
140140
CoinProtocol::QTUM => TrezorMessageType::Bitcoin,
141141
CoinProtocol::ETH { .. } | CoinProtocol::ERC20 { .. } => TrezorMessageType::Ethereum,
142142
_ => return Err(MmError::new(HDExtractPubkeyError::CoinDoesntSupportTrezor)),

mm2src/coins/lp_coins.rs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4300,11 +4300,14 @@ impl CoinsContext {
43004300
}
43014301

43024302
/// This enum is used in coin activation requests.
4303-
#[derive(Copy, Clone, Debug, Deserialize, Serialize, Default)]
4303+
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
43044304
pub enum PrivKeyActivationPolicy {
43054305
#[default]
43064306
ContextPrivKey,
43074307
Trezor,
4308+
WalletConnect {
4309+
session_topic: kdf_walletconnect::WcTopic,
4310+
},
43084311
}
43094312

43104313
impl PrivKeyActivationPolicy {
@@ -4362,10 +4365,16 @@ pub enum PrivKeyPolicy<T> {
43624365
/// - `public_key`: Compressed public key, represented as [H264].
43634366
/// - `public_key_uncompressed`: Uncompressed public key, represented as [H520].
43644367
/// - `session_topic`: WalletConnect session that was used to activate this coin.
4368+
// TODO: We want to have different variants of WalletConnect policy for different coin types:
4369+
// - ETH uses the structure found here.
4370+
// - Tendermint doesn't use this variant all together. Tendermint generalizes one level on top of PrivKeyPolicy by having a different activation policy
4371+
// structure that is either Priv(PrivKeyPolicy) or Pubkey(PublicKey) and when activated via wallet connect it uses the Pubkey(PublicKey) variant.
4372+
// - UTXO coins on the otherhand need to keep a list of all the addresses activated in the wallet and not just a single account.
4373+
// - 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).
43654374
WalletConnect {
43664375
public_key: H264,
43674376
public_key_uncompressed: H520,
4368-
session_topic: String,
4377+
session_topic: kdf_walletconnect::WcTopic,
43694378
},
43704379
}
43714380

@@ -4508,6 +4517,7 @@ pub enum PrivKeyBuildPolicy {
45084517
IguanaPrivKey(IguanaPrivKey),
45094518
GlobalHDAccount(GlobalHDAccountArc),
45104519
Trezor,
4520+
WalletConnect { session_topic: kdf_walletconnect::WcTopic },
45114521
}
45124522

45134523
impl PrivKeyBuildPolicy {
@@ -4683,11 +4693,21 @@ pub trait IguanaBalanceOps {
46834693
async fn iguana_balances(&self) -> BalanceResult<Self::BalanceObject>;
46844694
}
46854695

4696+
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
4697+
/// Information about the UTXO protocol used by a coin.
4698+
pub struct UtxoProtocolInfo {
4699+
/// A CAIP-2 compliant chain ID. Starts with `b122:`
4700+
/// This is used to identify the blockchain when using WalletConnect.
4701+
/// https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-4.md
4702+
chain_id: String,
4703+
}
4704+
46864705
#[allow(clippy::upper_case_acronyms)]
46874706
#[derive(Clone, Debug, Deserialize, Serialize)]
46884707
#[serde(tag = "type", content = "protocol_data")]
46894708
pub enum CoinProtocol {
4690-
UTXO,
4709+
// TODO: Nest this option deep into the innert struct fields when more fields are added to the UTXO protocol info.
4710+
UTXO(Option<UtxoProtocolInfo>),
46914711
QTUM,
46924712
QRC20 {
46934713
platform: String,
@@ -4764,7 +4784,7 @@ impl CoinProtocol {
47644784
CoinProtocol::TENDERMINTTOKEN(info) => Some(&info.platform),
47654785
#[cfg(not(target_arch = "wasm32"))]
47664786
CoinProtocol::LIGHTNING { platform, .. } => Some(platform),
4767-
CoinProtocol::UTXO
4787+
CoinProtocol::UTXO { .. }
47684788
| CoinProtocol::QTUM
47694789
| CoinProtocol::ETH { .. }
47704790
| CoinProtocol::TRX { .. }
@@ -4783,7 +4803,7 @@ impl CoinProtocol {
47834803
Some(contract_address)
47844804
},
47854805
CoinProtocol::SLPTOKEN { .. }
4786-
| CoinProtocol::UTXO
4806+
| CoinProtocol::UTXO { .. }
47874807
| CoinProtocol::QTUM
47884808
| CoinProtocol::ETH { .. }
47894809
| CoinProtocol::TRX { .. }
@@ -5075,7 +5095,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result<MmCoin
50755095
let protocol: CoinProtocol = try_s!(json::from_value(coins_en["protocol"].clone()));
50765096

50775097
let coin: MmCoinEnum = match &protocol {
5078-
CoinProtocol::UTXO => {
5098+
CoinProtocol::UTXO { .. } => {
50795099
let params = try_s!(UtxoActivationParams::from_legacy_req(req));
50805100
try_s!(utxo_standard_coin_with_policy(ctx, ticker, &coins_en, &params, priv_key_policy).await).into()
50815101
},
@@ -5748,7 +5768,7 @@ pub fn address_by_coin_conf_and_pubkey_str(
57485768
},
57495769
// Todo: implement TRX address generation
57505770
CoinProtocol::TRX { .. } => ERR!("TRX address generation is not implemented yet"),
5751-
CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => {
5771+
CoinProtocol::UTXO { .. } | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => {
57525772
utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format)
57535773
},
57545774
CoinProtocol::SLPTOKEN { platform, .. } => {

mm2src/coins/qrc20.rs

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use crate::utxo::rpc_clients::{
1212
#[cfg(not(target_arch = "wasm32"))]
1313
use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared};
1414
use crate::utxo::utxo_builder::{
15-
UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder,
16-
UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder,
15+
build_utxo_fields_with_global_hd, build_utxo_fields_with_iguana_priv_key, UtxoCoinBuildError, UtxoCoinBuildResult,
16+
UtxoCoinBuilder, UtxoCoinBuilderCommonOps,
1717
};
1818
use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_utxo_inputs_signed_by_pub, UtxoTxBuilder};
1919
use crate::utxo::{
@@ -35,7 +35,7 @@ use crate::{
3535
WithdrawResult,
3636
};
3737
use async_trait::async_trait;
38-
use bitcrypto::{dhash160, sha256};
38+
use bitcrypto::{dhash160, sha256, sign_message_hash};
3939
use chain::TransactionOutput;
4040
use common::executor::{AbortableSystem, AbortedError, Timer};
4141
use common::jsonrpc_client::{JsonRpcClient, JsonRpcRequest, RpcRes};
@@ -299,14 +299,6 @@ impl UtxoCoinBuilderCommonOps for Qrc20CoinBuilder<'_> {
299299
}
300300
}
301301

302-
impl UtxoFieldsWithIguanaSecretBuilder for Qrc20CoinBuilder<'_> {}
303-
304-
impl UtxoFieldsWithGlobalHDBuilder for Qrc20CoinBuilder<'_> {}
305-
306-
/// Although, `Qrc20Coin` doesn't support [`PrivKeyBuildPolicy::Trezor`] yet,
307-
/// `UtxoCoinBuilder` trait requires `UtxoFieldsWithHardwareWalletBuilder` to be implemented.
308-
impl UtxoFieldsWithHardwareWalletBuilder for Qrc20CoinBuilder<'_> {}
309-
310302
#[async_trait]
311303
impl UtxoCoinBuilder for Qrc20CoinBuilder<'_> {
312304
type ResultCoin = Qrc20Coin;
@@ -316,17 +308,27 @@ impl UtxoCoinBuilder for Qrc20CoinBuilder<'_> {
316308
self.priv_key_policy.clone()
317309
}
318310

319-
async fn build(self) -> MmResult<Self::ResultCoin, Self::Error> {
320-
let utxo = match self.priv_key_policy() {
321-
PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => self.build_utxo_fields_with_iguana_secret(priv_key).await?,
311+
async fn build_utxo_fields(&self) -> UtxoCoinBuildResult<UtxoCoinFields> {
312+
match self.priv_key_policy() {
313+
PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => build_utxo_fields_with_iguana_priv_key(self, priv_key).await,
322314
PrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => {
323-
self.build_utxo_fields_with_global_hd(global_hd_ctx).await?
315+
build_utxo_fields_with_global_hd(self, global_hd_ctx).await
324316
},
325317
PrivKeyBuildPolicy::Trezor => {
326318
let priv_key_err = PrivKeyPolicyNotAllowed::HardwareWalletNotSupported;
327-
return MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err));
319+
MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err))
328320
},
329-
};
321+
PrivKeyBuildPolicy::WalletConnect { .. } => {
322+
let priv_key_err = PrivKeyPolicyNotAllowed::UnsupportedMethod(
323+
"WalletConnect is not available for QRC20 coin".to_string(),
324+
);
325+
MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err))
326+
},
327+
}
328+
}
329+
330+
async fn build(self) -> MmResult<Self::ResultCoin, Self::Error> {
331+
let utxo = self.build_utxo_fields().await?;
330332

331333
let inner = Qrc20CoinFields {
332334
utxo,
@@ -1104,7 +1106,8 @@ impl MarketCoinOps for Qrc20Coin {
11041106
}
11051107

11061108
fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> {
1107-
utxo_common::sign_message_hash(self.as_ref(), message)
1109+
let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?;
1110+
Some(sign_message_hash(prefix, message))
11081111
}
11091112

11101113
fn sign_message(&self, message: &str, address: Option<HDAddressSelector>) -> SignatureResult<String> {

mm2src/coins/rpc_command/init_create_account.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,8 @@ impl RpcTask for InitCreateAccountTask {
310310
self.task_state.clone(),
311311
task_handle,
312312
utxo.is_trezor(),
313-
CoinProtocol::UTXO,
313+
// Note that the actual UtxoProtocolInfo isn't needed by trezor XPUB extractor.
314+
CoinProtocol::UTXO(Default::default()),
314315
)
315316
.await?,
316317
)),

mm2src/coins/rpc_command/offline_keys.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ fn extract_prefix_values(
140140

141141
match protocol {
142142
CoinProtocol::ETH { .. } | CoinProtocol::ERC20 { .. } | CoinProtocol::NFT { .. } => Ok(None),
143-
CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => {
143+
CoinProtocol::UTXO { .. } | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => {
144144
let wif_type = coin_conf["wiftype"]
145145
.as_u64()
146146
.ok_or_else(|| OfflineKeysError::MissingPrefixValue {

0 commit comments

Comments
 (0)