Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c5ccb30
refactor sign_message and enable sign message for specific address fo…
borngraced Apr 29, 2025
7c61845
implement support for utxo
borngraced Apr 29, 2025
529f2a6
add unit test for eth
borngraced Apr 30, 2025
8a42b0e
minor changes to eth test
borngraced Apr 30, 2025
b23b8fc
implement utxo unit test
borngraced Apr 30, 2025
d323812
apply onur's patch
borngraced Apr 30, 2025
ad7e0b5
fix review notes and validate coin type
borngraced May 1, 2025
032faff
Merge branch 'dev' into hd-multi-addr-msg-sign
borngraced May 1, 2025
6d4332e
impl Display for Bip32Deri..
borngraced May 1, 2025
9e9081e
nits
borngraced May 2, 2025
187284d
validate comps that makes up path_to_coin
borngraced May 3, 2025
b10fd57
impl path_to_coin method and use for rpc derivation_path validation
borngraced May 5, 2025
5fcef99
Merge branch 'dev' into hd-multi-addr-msg-sign
borngraced May 7, 2025
791c64b
rename WithdrawFrom and reuse in SignatureRequest -< message signing
borngraced May 8, 2025
8489bb0
fmt
borngraced May 8, 2025
67266b1
rename HdAccountIdentifier to AddressIdentifier
borngraced May 8, 2025
ca176d8
fmt
borngraced May 8, 2025
38eb5dd
Merge branch 'dev' into hd-multi-addr-msg-sign
borngraced May 20, 2025
b453126
fix review notes
borngraced May 22, 2025
e7add30
Merge branch 'dev' into hd-multi-addr-msg-sign
borngraced May 22, 2025
ecc6eb7
post merge fix
borngraced May 22, 2025
98a5b6c
Merge branch 'dev' into hd-multi-addr-msg-sign
borngraced May 26, 2025
df2bbed
rename AddressIdentifier to HDAddressSelector and update doc comments
borngraced May 27, 2025
935409d
apply segwit sign-verify patch by Omar
borngraced May 27, 2025
9f0bde4
cargo fmt
borngraced May 27, 2025
8180da3
impl unit test for address conflict caused by derivation_paths
borngraced May 27, 2025
4d91373
clippy
borngraced May 27, 2025
860c208
fix naming and minor changes
borngraced May 28, 2025
3ade61b
update lp_coins sign_message
borngraced May 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions mm2src/coins/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2378,10 +2378,25 @@ impl MarketCoinOps for EthCoin {
Some(keccak256(&stream.out()).take())
}

fn sign_message(&self, message: &str) -> SignatureResult<String> {
fn sign_message(&self, message: &str, address: Option<HDAddressSelector>) -> SignatureResult<String> {
let message_hash = self.sign_message_hash(message).ok_or(SignatureError::PrefixNotFound)?;
let privkey = &self.priv_key_policy.activated_key_or_err()?.secret();
let signature = sign(privkey, &H256::from(message_hash))?;

let secret = if let Some(address) = address {
let path_to_coin = self.priv_key_policy.path_to_coin_or_err()?;
let derivation_path = address
.valid_derivation_path(path_to_coin)
.mm_err(|err| SignatureError::InvalidRequest(err.to_string()))?;
let privkey = self
.priv_key_policy
.hd_wallet_derived_priv_key_or_err(&derivation_path)?;
ethkey::Secret::from_slice(privkey.as_slice()).ok_or(MmError::new(SignatureError::InternalError(
"failed to derive ethkey::Secret".to_string(),
)))?
} else {
self.priv_key_policy.activated_key_or_err()?.secret().clone()
};
let signature = sign(&secret, &H256::from(message_hash))?;

Ok(format!("0x{}", signature))
}

Expand Down
2 changes: 1 addition & 1 deletion mm2src/coins/eth/eth_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ fn test_sign_verify_message() {
);

let message = "test";
let signature = coin.sign_message(message).unwrap();
let signature = coin.sign_message(message, None).unwrap();
assert_eq!(signature, "0xcdf11a9c4591fb7334daa4b21494a2590d3f7de41c7d2b333a5b61ca59da9b311b492374cc0ba4fbae53933260fa4b1c18f15d95b694629a7b0620eec77a938600");

let is_valid = coin
Expand Down
8 changes: 5 additions & 3 deletions mm2src/coins/eth/eth_withdraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::{checksum_address, u256_to_big_decimal, wei_from_big_decimal, ChainSp
use crate::eth::{calc_total_fee, get_eth_gas_details_from_withdraw_fee, tx_builder_with_pay_for_gas_option,
tx_type_from_pay_for_gas_option, Action, Address, EthTxFeeDetails, KeyPair, PayForGasOption,
SignedEthTx, TransactionWrapper, UnSignedEthTxBuilder};
use crate::hd_wallet::{HDCoinWithdrawOps, HDWalletOps, WithdrawFrom, WithdrawSenderAddress};
use crate::hd_wallet::{HDAddressSelector, HDCoinWithdrawOps, HDWalletOps, WithdrawSenderAddress};
use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared};
use crate::{BytesJson, CoinWithDerivationMethod, EthCoin, GetWithdrawSenderAddress, PrivKeyPolicy, TransactionData,
TransactionDetails};
Expand Down Expand Up @@ -89,10 +89,12 @@ where

/// Gets the derivation path for the address from which the withdrawal is made using the `from` parameter.
#[allow(clippy::result_large_err)]
fn get_from_derivation_path(&self, from: &WithdrawFrom) -> Result<DerivationPath, MmError<WithdrawError>> {
fn get_from_derivation_path(&self, from: &HDAddressSelector) -> Result<DerivationPath, MmError<WithdrawError>> {
let coin = self.coin();
let path_to_coin = &coin.deref().derivation_method.hd_wallet_or_err()?.derivation_path;
let path_to_address = from.to_address_path(path_to_coin.coin_type())?;
let path_to_address = from
.to_address_path(path_to_coin.coin_type())
.mm_err(|err| WithdrawError::UnexpectedFromAddress(err.to_string()))?;
let derivation_path = path_to_address.to_derivation_path(path_to_coin)?;
Ok(derivation_path)
}
Expand Down
59 changes: 58 additions & 1 deletion mm2src/coins/hd_wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ mod wallet_ops;
pub use wallet_ops::HDWalletOps;

mod withdraw_ops;
pub use withdraw_ops::{HDCoinWithdrawOps, WithdrawFrom, WithdrawSenderAddress};
pub use withdraw_ops::{HDCoinWithdrawOps, WithdrawSenderAddress};

pub(crate) type HDAccountsMap<HDAccount> = BTreeMap<u32, HDAccount>;
pub(crate) type HDAccountsMutex<HDAccount> = AsyncMutex<HDAccountsMap<HDAccount>>;
Expand Down Expand Up @@ -485,6 +485,63 @@ impl HDPathAccountToAddressId {
Ok(account_der_path)
}
}
/// Represents how a hierarchical deterministic (HD) address is selected.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum HDAddressSelector {
/// Specifies the HD address using its structured account, chain, and address ID.
AddressId(HDPathAccountToAddressId),
/// Specifies the HD address directly using a BIP-44,84 and other compliant derivation path.
///
/// IMPORTANT: Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path,
/// `serde::Deserialize` returns "data did not match any variant of untagged enum HDAddressSelector".
/// It's better to show the user an informative error.
DerivationPath { derivation_path: String },
}

impl HDAddressSelector {
pub fn to_address_path(&self, expected_coin_type: u32) -> MmResult<HDPathAccountToAddressId, StandardHDPathError> {
match self {
HDAddressSelector::AddressId(address_id) => Ok(*address_id),
HDAddressSelector::DerivationPath { derivation_path } => {
let derivation_path = StandardHDPath::from_str(derivation_path).map_to_mm(StandardHDPathError::from)?;
let coin_type = derivation_path.coin_type();

if coin_type != expected_coin_type {
return MmError::err(StandardHDPathError::InvalidCoinType {
expected: expected_coin_type,
found: coin_type,
});
}

Ok(HDPathAccountToAddressId::from(derivation_path))
},
}
}

pub fn valid_derivation_path(self, path_to_coin: &HDPathToCoin) -> MmResult<DerivationPath, StandardHDPathError> {
match self {
HDAddressSelector::AddressId(id) => id
.to_derivation_path(path_to_coin)
.mm_err(StandardHDPathError::Bip32Error),
HDAddressSelector::DerivationPath { derivation_path } => {
let standard_hd_path = StandardHDPath::from_str(&derivation_path)
.map_to_mm(|_| StandardHDPathError::Bip32Error(Bip32Error::Decode))?;
let rpc_path_to_coin = standard_hd_path.path_to_coin();

// validate rpc path_to_coin against activated coin.
if &rpc_path_to_coin != path_to_coin {
return MmError::err(StandardHDPathError::InvalidPathToCoin {
expected: rpc_path_to_coin.to_string(),
found: path_to_coin.to_string(),
});
};

Ok(standard_hd_path.to_derivation_path())
},
}
}
}

pub(crate) mod inner_impl {
use super::*;
Expand Down
47 changes: 5 additions & 42 deletions mm2src/coins/hd_wallet/withdraw_ops.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,12 @@
use super::{DisplayAddress, HDPathAccountToAddressId, HDWalletOps, HDWithdrawError};
use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDWalletCoinOps};
use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDAddressSelector, HDCoinAddress, HDWalletCoinOps};
use async_trait::async_trait;
use bip32::DerivationPath;
use crypto::{StandardHDPath, StandardHDPathError};
use mm2_err_handle::prelude::*;
use std::str::FromStr;

type HDCoinPubKey<T> =
<<<<T as HDWalletCoinOps>::HDWallet as HDWalletOps>::HDAccount as HDAccountOps>::HDAddress as HDAddressOps>::Pubkey;

/// Represents the source of the funds for a withdrawal operation.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum WithdrawFrom {
/// The address id of the sender address which is specified by the account id, chain, and address id.
AddressId(HDPathAccountToAddressId),
/// The derivation path of the sender address in the BIP-44 format.
///
/// IMPORTANT: Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path,
/// `serde::Deserialize` returns "data did not match any variant of untagged enum WithdrawFrom".
/// It's better to show the user an informative error.
DerivationPath { derivation_path: String },
}

impl WithdrawFrom {
#[allow(clippy::result_large_err)]
pub fn to_address_path(&self, expected_coin_type: u32) -> MmResult<HDPathAccountToAddressId, HDWithdrawError> {
match self {
WithdrawFrom::AddressId(address_id) => Ok(*address_id),
WithdrawFrom::DerivationPath { derivation_path } => {
let derivation_path = StandardHDPath::from_str(derivation_path)
.map_to_mm(StandardHDPathError::from)
.mm_err(|e| HDWithdrawError::UnexpectedFromAddress(e.to_string()))?;
let coin_type = derivation_path.coin_type();
if coin_type != expected_coin_type {
let error = format!(
"Derivation path '{}' must have '{}' coin type",
derivation_path, expected_coin_type
);
return MmError::err(HDWithdrawError::UnexpectedFromAddress(error));
}
Ok(HDPathAccountToAddressId::from(derivation_path))
},
}
}
}

/// Contains the details of the sender address for a withdraw operation.
pub struct WithdrawSenderAddress<Address, Pubkey> {
pub(crate) address: Address,
Expand All @@ -61,13 +22,15 @@ pub trait HDCoinWithdrawOps: HDWalletCoinOps {
async fn get_withdraw_hd_sender(
&self,
hd_wallet: &Self::HDWallet,
from: &WithdrawFrom,
from: &HDAddressSelector,
) -> MmResult<WithdrawSenderAddress<HDCoinAddress<Self>, HDCoinPubKey<Self>>, HDWithdrawError> {
let HDPathAccountToAddressId {
account_id,
chain,
address_id,
} = from.to_address_path(hd_wallet.coin_type())?;
} = from
.to_address_path(hd_wallet.coin_type())
.mm_err(|err| HDWithdrawError::UnexpectedFromAddress(err.to_string()))?;

let hd_account = hd_wallet
.get_account(account_id)
Expand Down
8 changes: 7 additions & 1 deletion mm2src/coins/lightning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod ln_storage;
pub mod ln_utils;

use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult};
use crate::hd_wallet::HDAddressSelector;
use crate::lightning::ln_utils::{filter_channels, pay_invoice_with_max_total_cltv_expiry_delta, PaymentError};
use crate::utxo::rpc_clients::UtxoRpcClientEnum;
use crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned};
Expand Down Expand Up @@ -956,7 +957,12 @@ impl MarketCoinOps for LightningCoin {
Some(dhash256(prefixed_message.as_bytes()).take())
}

fn sign_message(&self, message: &str) -> SignatureResult<String> {
fn sign_message(&self, message: &str, address: Option<HDAddressSelector>) -> SignatureResult<String> {
if address.is_some() {
return MmError::err(SignatureError::InvalidRequest(
"functionality not supported for Lightning yet.".into(),
));
}
let message_hash = self.sign_message_hash(message).ok_or(SignatureError::PrefixNotFound)?;
let secret_key = self
.keys_manager
Expand Down
25 changes: 16 additions & 9 deletions mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,9 @@ use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetail
GetEthAddressError, GetValidEthWithdrawAddError, SignedEthTx};

pub mod hd_wallet;
use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress,
HDCoinHDAccount, HDExtractPubkeyError, HDPathAccountToAddressId, HDWalletAddress, HDWalletCoinOps,
HDWalletOps, HDWithdrawError, HDXPubExtractor, WithdrawFrom, WithdrawSenderAddress};
use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps,
HDAddressSelector, HDCoinAddress, HDCoinHDAccount, HDExtractPubkeyError, HDPathAccountToAddressId,
HDWalletAddress, HDWalletCoinOps, HDWalletOps, HDWithdrawError, HDXPubExtractor, WithdrawSenderAddress};

#[cfg(not(target_arch = "wasm32"))] pub mod lightning;
#[cfg_attr(target_arch = "wasm32", allow(dead_code, unused_imports))]
Expand Down Expand Up @@ -2083,7 +2083,7 @@ pub trait MarketCoinOps {

fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]>;

fn sign_message(&self, _message: &str) -> SignatureResult<String>;
fn sign_message(&self, _message: &str, _address: Option<HDAddressSelector>) -> SignatureResult<String>;

fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult<bool>;

Expand Down Expand Up @@ -2210,7 +2210,7 @@ pub trait GetWithdrawSenderAddress {
#[derive(Clone, Default, Deserialize)]
pub struct WithdrawRequest {
coin: String,
from: Option<WithdrawFrom>,
from: Option<HDAddressSelector>,
to: String,
#[serde(default)]
amount: BigDecimal,
Expand Down Expand Up @@ -2294,10 +2294,11 @@ pub enum ValidatorsInfoDetails {
Cosmos(rpc_command::tendermint::staking::ValidatorsQuery),
}

#[derive(Serialize, Deserialize)]
#[derive(Deserialize)]
pub struct SignatureRequest {
coin: String,
message: String,
address: Option<HDAddressSelector>,
}

#[derive(Serialize, Deserialize)]
Expand Down Expand Up @@ -5116,8 +5117,14 @@ pub async fn get_raw_transaction(ctx: MmArc, req: RawTransactionRequest) -> RawT
}

pub async fn sign_message(ctx: MmArc, req: SignatureRequest) -> SignatureResult<SignatureResponse> {
if req.address.is_some() && !ctx.enable_hd() {
return MmError::err(SignatureError::InvalidRequest(
"You need to enable kdf with enable_hd to sign messages with a specific account/address".to_string(),
));
};
let coin = lp_coinfind_or_err(&ctx, &req.coin).await?;
let signature = coin.sign_message(&req.message)?;
let signature = coin.sign_message(&req.message, req.address)?;

Ok(SignatureResponse { signature })
}

Expand Down Expand Up @@ -6151,7 +6158,7 @@ mod tests {
pub mod for_tests {
use crate::rpc_command::init_withdraw::WithdrawStatusRequest;
use crate::rpc_command::init_withdraw::{init_withdraw, withdraw_status};
use crate::{TransactionDetails, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawRequest};
use crate::{HDAddressSelector, TransactionDetails, WithdrawError, WithdrawFee, WithdrawRequest};
use common::executor::Timer;
use common::{now_ms, wait_until_ms};
use mm2_core::mm_ctx::MmArc;
Expand All @@ -6173,7 +6180,7 @@ pub mod for_tests {
client_id: 0,
inner: WithdrawRequest {
amount: BigDecimal::from_str(amount).unwrap(),
from: from_derivation_path.map(|from_derivation_path| WithdrawFrom::DerivationPath {
from: from_derivation_path.map(|from_derivation_path| HDAddressSelector::DerivationPath {
derivation_path: from_derivation_path.to_owned(),
}),
to: to.to_owned(),
Expand Down
5 changes: 3 additions & 2 deletions mm2src/coins/qrc20.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentResult};
use crate::eth::{self, u256_to_big_decimal, wei_from_big_decimal, TryToAddress};
use crate::hd_wallet::HDAddressSelector;
use crate::qrc20::rpc_clients::{LogEntry, Qrc20ElectrumOps, Qrc20NativeOps, Qrc20RpcOps, TopicFilter, TxReceipt,
ViewContractCallType};
use crate::utxo::qtum::QtumBasedCoin;
Expand Down Expand Up @@ -1039,8 +1040,8 @@ impl MarketCoinOps for Qrc20Coin {
utxo_common::sign_message_hash(self.as_ref(), message)
}

fn sign_message(&self, message: &str) -> SignatureResult<String> {
utxo_common::sign_message(self.as_ref(), message)
fn sign_message(&self, message: &str, address: Option<HDAddressSelector>) -> SignatureResult<String> {
utxo_common::sign_message(self.as_ref(), message, address)
}

fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult<bool> {
Expand Down
5 changes: 3 additions & 2 deletions mm2src/coins/rpc_command/tendermint/staking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use cosmrs::staking::{Commission, Description, Validator};
use mm2_err_handle::prelude::MmError;
use mm2_number::BigDecimal;

use crate::{hd_wallet::WithdrawFrom, tendermint::TendermintCoinRpcError, MmCoinEnum, StakingInfoError, WithdrawFee};
use crate::{hd_wallet::HDAddressSelector, tendermint::TendermintCoinRpcError, MmCoinEnum, StakingInfoError,
WithdrawFee};

/// Represents current status of the validator.
#[derive(Debug, Default, Deserialize)]
Expand Down Expand Up @@ -131,7 +132,7 @@ pub async fn validators_rpc(
pub struct DelegationPayload {
pub validator_address: String,
pub fee: Option<WithdrawFee>,
pub withdraw_from: Option<WithdrawFrom>,
pub withdraw_from: Option<HDAddressSelector>,
#[serde(default)]
pub memo: String,
#[serde(default)]
Expand Down
14 changes: 10 additions & 4 deletions mm2src/coins/siacoin.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{BalanceError, CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut,
RawTransactionRequest, SwapOps, TradeFee, TransactionEnum};
use crate::hd_wallet::HDAddressSelector;
use crate::{coin_errors::MyAddressError, AddressFromPubkeyError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs,
ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr,
PrivKeyBuildPolicy, PrivKeyPolicy, RawTransactionResult, RefundPaymentArgs, SearchForSwapTxSpendInput,
Expand All @@ -8,6 +9,7 @@ use crate::{coin_errors::MyAddressError, AddressFromPubkeyError, BalanceFut, Can
ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentInput,
ValidatePaymentResult, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawFut,
WithdrawRequest};
use crate::{SignatureError, VerificationError};
use async_trait::async_trait;
use common::executor::AbortedError;
pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature};
Expand Down Expand Up @@ -321,14 +323,18 @@ impl MarketCoinOps for SiaCoin {
Ok(address.to_string())
}

async fn get_public_key(&self) -> Result<String, MmError<UnexpectedDerivationMethod>> { unimplemented!() }
async fn get_public_key(&self) -> Result<String, MmError<UnexpectedDerivationMethod>> {
MmError::err(UnexpectedDerivationMethod::InternalError("Not implemented".into()))
}

fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() }
fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { None }

fn sign_message(&self, _message: &str) -> SignatureResult<String> { unimplemented!() }
fn sign_message(&self, _message: &str, _address: Option<HDAddressSelector>) -> SignatureResult<String> {
MmError::err(SignatureError::InternalError("Not implemented".into()))
}

fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult<bool> {
unimplemented!()
MmError::err(VerificationError::InternalError("Not implemented".into()))
}

fn my_balance(&self) -> BalanceFut<CoinBalance> {
Expand Down
Loading
Loading