diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index c9672082c7..c8ae6fe874 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -81,6 +81,10 @@ impl From for ValidatePaymentError { } } +impl From for ValidatePaymentError { + fn from(err: keys::Error) -> Self { Self::InternalError(err.to_string()) } +} + #[derive(Debug, Display)] pub enum MyAddressError { UnexpectedDerivationMethod(String), diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs index d823f00f8a..aab33e9088 100644 --- a/mm2src/coins/lightning/ln_events.rs +++ b/mm2src/coins/lightning/ln_events.rs @@ -191,7 +191,7 @@ pub enum SignFundingTransactionError { // Generates the raw funding transaction with one output equal to the channel value. fn sign_funding_transaction( uuid: Uuid, - output_script: &Script, + output_script_pubkey: &Script, platform: Arc, ) -> Result { let coin = &platform.coin; @@ -207,7 +207,7 @@ fn sign_funding_transaction( })? .clone() }; - unsigned.outputs[0].script_pubkey = output_script.to_bytes().into(); + unsigned.outputs[0].script_pubkey = output_script_pubkey.to_bytes().into(); let my_address = coin .as_ref() @@ -532,7 +532,17 @@ impl LightningEventHandler { let keys_manager = self.keys_manager.clone(); let fut = async move { - let change_destination_script = Builder::build_p2witness(&my_address.hash).to_bytes().take().into(); + let change_destination_script = match Builder::build_p2wpkh(my_address.hash()) { + Ok(script) => script.to_bytes().take().into(), + Err(err) => { + error!( + "Could not create witness script for change output {}: {}", + my_address.to_string(), + err.to_string() + ); + return; + }, + }; let feerate_sat_per_1000_weight = platform.get_est_sat_per_1000_weight(ConfirmationTarget::Normal); let output_descriptors = outputs.iter().collect::>(); let claiming_tx = match keys_manager.spend_spendable_outputs( diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 1ca02100a7..eb477916ea 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -657,6 +657,10 @@ impl TransactionErr { } } +impl From for TransactionErr { + fn from(e: keys::Error) -> Self { TransactionErr::Plain(e.to_string()) } +} + #[derive(Debug, PartialEq)] pub enum FoundSwapTxSpend { Spent(TransactionEnum), @@ -4258,7 +4262,7 @@ struct ConvertUtxoAddressReq { pub async fn convert_utxo_address(ctx: MmArc, req: Json) -> Result>, String> { let req: ConvertUtxoAddressReq = try_s!(json::from_value(req)); - let mut addr: utxo::Address = try_s!(req.address.parse()); + let mut addr: utxo::LegacyAddress = try_s!(req.address.parse()); // Only legacy addresses supported as source let coin = match lp_coinfind(&ctx, &req.to_coin).await { Ok(Some(c)) => c, _ => return ERR!("Coin {} is not activated", req.to_coin), @@ -4267,8 +4271,7 @@ pub async fn convert_utxo_address(ctx: MmArc, req: Json) -> Result utxo, _ => return ERR!("Coin {} is not utxo", req.to_coin), }; - addr.prefix = coin.as_ref().conf.pub_addr_prefix; - addr.t_addr_prefix = coin.as_ref().conf.pub_t_addr_prefix; + addr.prefix = coin.as_ref().conf.address_prefixes.p2pkh.clone(); addr.checksum_type = coin.as_ref().conf.checksum_type; let response = try_s!(json::to_vec(&json!({ diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 10869db6f8..bb37c9868e 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -44,7 +44,7 @@ use futures::compat::Future01CompatExt; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use keys::bytes::Bytes as ScriptBytes; -use keys::{Address as UtxoAddress, Address, KeyPair, Public}; +use keys::{Address as UtxoAddress, KeyPair, Public}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; @@ -634,21 +634,21 @@ impl UtxoTxGenerationOps for Qrc20Coin { impl GetUtxoListOps for Qrc20Coin { async fn get_unspent_ordered_list( &self, - address: &Address, + address: &UtxoAddress, ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { utxo_common::get_unspent_ordered_list(self, address).await } async fn get_all_unspent_ordered_list( &self, - address: &Address, + address: &UtxoAddress, ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { utxo_common::get_all_unspent_ordered_list(self, address).await } async fn get_mature_unspent_ordered_list( &self, - address: &Address, + address: &UtxoAddress, ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { utxo_common::get_mature_unspent_ordered_list(self, address).await } @@ -675,8 +675,8 @@ impl UtxoCommonOps for Qrc20Coin { utxo_common::checked_address_from_str(self, address) } - fn script_for_address(&self, address: &Address) -> MmResult { - utxo_common::get_script_for_address(self.as_ref(), address) + fn script_for_address(&self, address: &UtxoAddress) -> MmResult { + utxo_common::output_script_checked(self.as_ref(), address) } async fn get_current_mtp(&self) -> UtxoRpcResult { @@ -748,12 +748,11 @@ impl UtxoCommonOps for Qrc20Coin { utxo_common::addr_format_for_standard_scripts(self) } - fn address_from_pubkey(&self, pubkey: &Public) -> Address { + fn address_from_pubkey(&self, pubkey: &Public) -> UtxoAddress { let conf = &self.utxo.conf; utxo_common::address_from_pubkey( pubkey, - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + conf.address_prefixes.clone(), conf.checksum_type, conf.bech32_hrp.clone(), self.addr_format().clone(), @@ -1537,12 +1536,10 @@ pub struct Qrc20FeeDetails { } async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult { - let to_addr = UtxoAddress::from_str(&req.to) - .map_err(|e| e.to_string()) + let to_addr = UtxoAddress::from_legacyaddress(&req.to, &coin.as_ref().conf.address_prefixes) .map_to_mm(WithdrawError::InvalidAddress)?; let conf = &coin.utxo.conf; - let is_p2pkh = to_addr.prefix == conf.pub_addr_prefix && to_addr.t_addr_prefix == conf.pub_t_addr_prefix; - if !is_p2pkh { + if !to_addr.is_pubkey_hash() { let error = "QRC20 can be sent to P2PKH addresses only".to_owned(); return MmError::err(WithdrawError::InvalidAddress(error)); } diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index a837e18364..4ae8f7601e 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -5,6 +5,7 @@ use chain::OutPoint; use common::{block_on, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::Secp256k1Secret; use itertools::Itertools; +use keys::{Address, AddressBuilder}; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_number::bigdecimal::Zero; use mocktopus::mocking::{MockResult, Mockable}; @@ -65,14 +66,16 @@ fn test_withdraw_to_p2sh_address_should_fail() { ]; let (_, coin) = qrc20_coin_for_test(priv_key, None); - let p2sh_address = Address { - prefix: coin.as_ref().conf.p2sh_addr_prefix, - hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), - t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Standard, - }; + let p2sh_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + coin.as_ref().derivation_method.unwrap_single_addr().hash().clone(), + *coin.as_ref().derivation_method.unwrap_single_addr().checksum_type(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh() + .build() + .expect("valid address props"); let req = WithdrawRequest { amount: 10.into(), @@ -150,7 +153,11 @@ fn test_validate_maker_payment() { assert_eq!( *coin.utxo.derivation_method.unwrap_single_addr(), - "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf".into() + Address::from_legacyaddress( + "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf", + &coin.as_ref().conf.address_prefixes + ) + .unwrap() ); // tx_hash: 016a59dd2b181b3906b0f0333d5c7561dacb332dc99ac39679a591e523f2c49a @@ -249,7 +256,11 @@ fn test_wait_for_confirmations_excepted() { assert_eq!( *coin.utxo.derivation_method.unwrap_single_addr(), - "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf".into() + Address::from_legacyaddress( + "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf", + &coin.as_ref().conf.address_prefixes + ) + .unwrap() ); // tx_hash: 35e03bc529528a853ee75dde28f27eec8ed7b152b6af7ab6dfa5d55ea46f25ac @@ -557,7 +568,11 @@ fn test_generate_token_transfer_script_pubkey() { gas_price, }; - let to_addr: UtxoAddress = "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(); + let to_addr: UtxoAddress = UtxoAddress::from_legacyaddress( + "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); let to_addr = qtum::contract_addr_from_utxo_addr(to_addr).unwrap(); let amount: U256 = 1000000000.into(); let actual = coin.transfer_output(to_addr, amount, gas_limit, gas_price).unwrap(); diff --git a/mm2src/coins/qrc20/script_pubkey.rs b/mm2src/coins/qrc20/script_pubkey.rs index 08abad3024..c84455cde4 100644 --- a/mm2src/coins/qrc20/script_pubkey.rs +++ b/mm2src/coins/qrc20/script_pubkey.rs @@ -193,6 +193,8 @@ fn decode_contract_number(source: &[u8]) -> Result { #[cfg(test)] mod tests { + use keys::prefixes::QRC20_PREFIXES; + use super::*; #[test] @@ -246,7 +248,8 @@ mod tests { fn test_extract_contract_call() { let script: Script = "5403a02526012844a9059cbb0000000000000000000000000240b898276ad2cc0d2fe6f527e8e31104e7fde3000000000000000000000000000000000000000000000000000000003b9aca0014d362e096e873eb7907e205fadc6175c6fec7bc44c2".into(); - let to_addr: UtxoAddress = "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(); + let to_addr: UtxoAddress = + UtxoAddress::from_legacyaddress("qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs", &QRC20_PREFIXES).unwrap(); let to_addr = qtum::contract_addr_from_utxo_addr(to_addr).unwrap(); let amount: U256 = 1000000000.into(); let function = eth::ERC20_CONTRACT.function("transfer").unwrap(); diff --git a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs index 7f0c1e4ce9..b90866d6b2 100644 --- a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs +++ b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs @@ -1,5 +1,6 @@ use crate::coin_balance::HDAddressBalance; use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::utxo::utxo_common; use crate::{lp_coinfind_or_err, CoinsContext, MmCoinEnum}; use async_trait::async_trait; use common::{SerdeInfallible, SuccessResponse}; @@ -132,10 +133,8 @@ pub mod common_impl { use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; use crate::utxo::UtxoCommonOps; use crate::CoinWithDerivationMethod; - use keys::Address; use std::collections::HashSet; use std::ops::DerefMut; - use std::str::FromStr; pub async fn scan_for_new_addresses_rpc( coin: &Coin, @@ -165,7 +164,9 @@ pub mod common_impl { let addresses: HashSet<_> = new_addresses .iter() - .map(|address_balance| Address::from_str(&address_balance.address).expect("Valid address")) + .map(|address_balance| { + utxo_common::address_from_str_unchecked(coin.as_ref(), &address_balance.address).expect("Valid address") + }) .collect(); coin.prepare_addresses_for_balance_stream_if_enabled(addresses.into()) diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs index bcabd615a7..f0e7b48bd7 100644 --- a/mm2src/coins/rpc_command/lightning/open_channel.rs +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -161,7 +161,10 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes // The actual script_pubkey will replace this before signing the transaction after receiving the required // output script from the other node when the channel is accepted - let script_pubkey = Builder::build_p2witness(&AddressHashEnum::WitnessScriptHash(Default::default())).to_bytes(); + let script_pubkey = match Builder::build_p2wsh(&AddressHashEnum::WitnessScriptHash(Default::default())) { + Ok(script) => script.to_bytes(), + Err(err) => return MmError::err(OpenChannelError::InternalError(err.to_string())), + }; let outputs = vec![TransactionOutput { value, script_pubkey }]; let mut tx_builder = UtxoTxBuilder::new(&platform_coin) diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 79147f034e..c758512271 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -61,9 +61,10 @@ use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures01::Future; use keys::bytes::Bytes; +use keys::NetworkAddressPrefixes; use keys::Signature; -pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, - Type as ScriptType}; +pub use keys::{Address, AddressBuilder, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressPrefix, + AddressScriptType, KeyPair, LegacyAddress, Private, Public, Secret}; #[cfg(not(target_arch = "wasm32"))] use lightning_invoice::Currency as LightningCurrency; use mm2_core::mm_ctx::{MmArc, MmWeak}; @@ -201,6 +202,10 @@ impl From for BalanceError { } } +impl From for BalanceError { + fn from(e: keys::Error) -> Self { BalanceError::Internal(e.to_string()) } +} + impl From for WithdrawError { fn from(e: UtxoRpcError) -> Self { match e { @@ -504,11 +509,8 @@ pub struct UtxoCoinConf { pub ticker: String, /// https://en.bitcoin.it/wiki/List_of_address_prefixes /// https://github.com/jl777/coins/blob/master/coins - pub pub_addr_prefix: u8, - pub p2sh_addr_prefix: u8, pub wif_prefix: u8, - pub pub_t_addr_prefix: u8, - pub p2sh_t_addr_prefix: u8, + pub address_prefixes: NetworkAddressPrefixes, pub sign_message_prefix: Option, // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Segwit_address_format pub bech32_hrp: Option, @@ -646,12 +648,18 @@ pub enum UnsupportedAddr { HrpError { ticker: String, hrp: String }, #[display(fmt = "Segwit not activated in the config for {}", _0)] SegwitNotActivated(String), + #[display(fmt = "Internal error {}", _0)] + InternalError(String), } impl From for WithdrawError { fn from(e: UnsupportedAddr) -> Self { WithdrawError::InvalidAddress(e.to_string()) } } +impl From for UnsupportedAddr { + fn from(e: keys::Error) -> Self { UnsupportedAddr::InternalError(e.to_string()) } +} + #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum GetTxError { @@ -879,7 +887,7 @@ impl HDAddressBalanceScanner for UtxoAddressScanner { let is_used = match self { UtxoAddressScanner::Native { non_empty_addresses } => non_empty_addresses.contains(&address.to_string()), UtxoAddressScanner::Electrum(electrum_client) => { - let script = output_script(address, ScriptType::P2PKH); + let script = output_script(address)?; let script_hash = electrum_script_hash(&script); let electrum_history = electrum_client @@ -1265,6 +1273,10 @@ impl From for GenerateTxError { fn from(e: NumConversError) -> Self { GenerateTxError::Internal(e.to_string()) } } +impl From for GenerateTxError { + fn from(e: keys::Error) -> Self { GenerateTxError::Internal(e.to_string()) } +} + pub enum RequestTxHistoryResult { Ok(Vec<(H256Json, u64)>), Retry { error: String }, @@ -1881,12 +1893,12 @@ where }) .collect(); - let signature_version = match &my_address.addr_format { + let signature_version = match my_address.addr_format() { UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, _ => coin.as_ref().conf.signature_version, }; - let prev_script = utxo_common::get_script_for_address(coin.as_ref(), my_address) + let prev_script = utxo_common::output_script_checked(coin.as_ref(), my_address) .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let signed = try_tx_s!(sign_tx( unsigned, @@ -1903,15 +1915,13 @@ where Ok(signed) } -pub fn output_script(address: &Address, script_type: ScriptType) -> Script { - match address.addr_format { - UtxoAddressFormat::Segwit => Builder::build_p2witness(&address.hash), - _ => match script_type { - ScriptType::P2PKH => Builder::build_p2pkh(&address.hash), - ScriptType::P2SH => Builder::build_p2sh(&address.hash), - ScriptType::P2WPKH => Builder::build_p2witness(&address.hash), - ScriptType::P2WSH => Builder::build_p2witness(&address.hash), - }, +/// Builds transaction output script for an Address struct +pub fn output_script(address: &Address) -> Result { + match address.script_type() { + AddressScriptType::P2PKH => Ok(Builder::build_p2pkh(address.hash())), + AddressScriptType::P2SH => Ok(Builder::build_p2sh(address.hash())), + AddressScriptType::P2WPKH => Builder::build_p2wpkh(address.hash()), + AddressScriptType::P2WSH => Builder::build_p2wsh(address.hash()), } } @@ -1941,14 +1951,15 @@ pub fn address_by_conf_and_pubkey_str( let pubkey_bytes = try_s!(hex::decode(pubkey)); let hash = dhash160(&pubkey_bytes); - let address = Address { - prefix: utxo_conf.pub_addr_prefix, - t_addr_prefix: utxo_conf.pub_t_addr_prefix, - hash: hash.into(), - checksum_type: utxo_conf.checksum_type, - hrp: utxo_conf.bech32_hrp, + let address = AddressBuilder::new( addr_format, - }; + hash.into(), + utxo_conf.checksum_type, + utxo_conf.address_prefixes, + utxo_conf.bech32_hrp, + ) + .as_pkh() + .build()?; address.display_address() } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index f9c9d18b11..57170c34a2 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -157,11 +157,7 @@ impl BchCoin { pub fn slp_address(&self, address: &Address) -> Result { let conf = &self.as_ref().conf; - address.to_cashaddress( - &self.slp_prefix().to_string(), - conf.pub_addr_prefix, - conf.p2sh_addr_prefix, - ) + address.to_cashaddress(&self.slp_prefix().to_string(), &conf.address_prefixes) } pub fn bchd_urls(&self) -> &[String] { &self.bchd_urls } @@ -348,11 +344,8 @@ impl BchCoin { pub fn get_my_slp_address(&self) -> Result { let my_address = try_s!(self.as_ref().derivation_method.single_addr_or_err()); - let slp_address = my_address.to_cashaddress( - &self.slp_prefix().to_string(), - self.as_ref().conf.pub_addr_prefix, - self.as_ref().conf.p2sh_addr_prefix, - )?; + let slp_address = + my_address.to_cashaddress(&self.slp_prefix().to_string(), &self.as_ref().conf.address_prefixes)?; Ok(slp_address) } @@ -760,7 +753,7 @@ impl UtxoCommonOps for BchCoin { } fn script_for_address(&self, address: &Address) -> MmResult { - utxo_common::get_script_for_address(self.as_ref(), address) + utxo_common::output_script_checked(self.as_ref(), address) } async fn get_current_mtp(&self) -> UtxoRpcResult { @@ -833,8 +826,7 @@ impl UtxoCommonOps for BchCoin { let addr_format = self.addr_format().clone(); utxo_common::address_from_pubkey( pubkey, - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + conf.address_prefixes.clone(), conf.checksum_type, conf.bech32_hrp.clone(), addr_format, diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 1ab7fe1a0d..dc9e6fda0b 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -113,26 +113,19 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { /// Try to parse address from either wallet (UTXO) format or contract format. fn utxo_address_from_any_format(&self, from: &str) -> Result { - let utxo_err = match Address::from_str(from) { + let utxo_err = match Address::from_legacyaddress(from, &self.as_ref().conf.address_prefixes) { Ok(addr) => { - let is_p2pkh = addr.prefix == self.as_ref().conf.pub_addr_prefix - && addr.t_addr_prefix == self.as_ref().conf.pub_t_addr_prefix; - if is_p2pkh { + if addr.is_pubkey_hash() { return Ok(addr); } - "Address has invalid prefixes".to_string() + "Address has invalid prefix".to_string() }, - Err(e) => e.to_string(), + Err(e) => e, }; - let utxo_segwit_err = match Address::from_segwitaddress( - from, - self.as_ref().conf.checksum_type, - self.as_ref().conf.pub_addr_prefix, - self.as_ref().conf.pub_t_addr_prefix, - ) { + let utxo_segwit_err = match Address::from_segwitaddress(from, self.as_ref().conf.checksum_type) { Ok(addr) => { let is_segwit = - addr.hrp.is_some() && addr.hrp == self.as_ref().conf.bech32_hrp && self.as_ref().conf.segwit; + addr.hrp().is_some() && addr.hrp() == &self.as_ref().conf.bech32_hrp && self.as_ref().conf.segwit; if is_segwit { return Ok(addr); } @@ -154,14 +147,16 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { fn utxo_addr_from_contract_addr(&self, address: H160) -> Address { let utxo = self.as_ref(); - Address { - prefix: utxo.conf.pub_addr_prefix, - t_addr_prefix: utxo.conf.pub_t_addr_prefix, - hash: AddressHashEnum::AddressHash(address.0.into()), - checksum_type: utxo.conf.checksum_type, - hrp: utxo.conf.bech32_hrp.clone(), - addr_format: self.addr_format().clone(), - } + AddressBuilder::new( + self.addr_format().clone(), + AddressHashEnum::AddressHash(address.0.into()), + utxo.conf.checksum_type, + utxo.conf.address_prefixes.clone(), + utxo.conf.bech32_hrp.clone(), + ) + .as_pkh() + .build() + .expect("valid address props") } fn my_addr_as_contract_addr(&self) -> MmResult { @@ -171,22 +166,23 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { fn utxo_address_from_contract_addr(&self, address: H160) -> Address { let utxo = self.as_ref(); - Address { - prefix: utxo.conf.pub_addr_prefix, - t_addr_prefix: utxo.conf.pub_t_addr_prefix, - hash: AddressHashEnum::AddressHash(address.0.into()), - checksum_type: utxo.conf.checksum_type, - hrp: utxo.conf.bech32_hrp.clone(), - addr_format: self.addr_format().clone(), - } + AddressBuilder::new( + self.addr_format().clone(), + AddressHashEnum::AddressHash(address.0.into()), + utxo.conf.checksum_type, + utxo.conf.address_prefixes.clone(), + utxo.conf.bech32_hrp.clone(), + ) + .as_pkh() + .build() + .expect("valid address props") } fn contract_address_from_raw_pubkey(&self, pubkey: &[u8]) -> Result { let utxo = self.as_ref(); let qtum_address = try_s!(utxo_common::address_from_raw_pubkey( pubkey, - utxo.conf.pub_addr_prefix, - utxo.conf.pub_t_addr_prefix, + utxo.conf.address_prefixes.clone(), utxo.conf.checksum_type, utxo.conf.bech32_hrp.clone(), self.addr_format().clone() @@ -421,7 +417,7 @@ impl UtxoCommonOps for QtumCoin { } fn script_for_address(&self, address: &Address) -> MmResult { - utxo_common::get_script_for_address(self.as_ref(), address) + utxo_common::output_script_checked(self.as_ref(), address) } async fn get_current_mtp(&self) -> UtxoRpcResult { @@ -497,8 +493,7 @@ impl UtxoCommonOps for QtumCoin { let conf = &self.utxo_arc.conf; utxo_common::address_from_pubkey( pubkey, - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + conf.address_prefixes.clone(), conf.checksum_type, conf.bech32_hrp.clone(), self.addr_format().clone(), @@ -1312,7 +1307,7 @@ impl UtxoTxHistoryOps for QtumCoin { pub fn contract_addr_from_str(addr: &str) -> Result { eth::addr_from_str(addr) } pub fn contract_addr_from_utxo_addr(address: Address) -> MmResult { - match address.hash { + match address.hash() { AddressHashEnum::AddressHash(h) => Ok(h.take().into()), AddressHashEnum::WitnessScriptHash(_) => MmError::err(ScriptHashTypeNotSupported { script_hash_type: "Witness".to_owned(), diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index e602dbcc5b..f146042112 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -218,7 +218,7 @@ impl QtumCoin { amount, staker, am_i_staking, - is_staking_supported: !my_address.addr_format.is_segwit(), + is_staking_supported: !my_address.addr_format().is_segwit(), } .into(), }; @@ -234,14 +234,14 @@ impl QtumCoin { if let Some(staking_addr) = self.am_i_currently_staking().await? { return MmError::err(DelegationError::AlreadyDelegating(staking_addr)); } - let to_addr = - Address::from_str(request.address.as_str()).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; + let to_addr = Address::from_legacyaddress(request.address.as_str(), &self.as_ref().conf.address_prefixes) + .map_to_mm(DelegationError::AddressError)?; let fee = request.fee.unwrap_or(QTUM_DELEGATION_STANDARD_FEE); let _utxo_lock = UTXO_LOCK.lock(); let staker_address_hex = qtum::contract_addr_from_utxo_addr(to_addr.clone())?; let delegation_output = self.add_delegation_output( staker_address_hex, - to_addr.hash, + to_addr.hash().clone(), fee, QRC20_GAS_LIMIT_DELEGATION, QRC20_GAS_PRICE_DEFAULT, diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index c8772066fe..855714d85d 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -28,7 +28,7 @@ use futures01::{Future, Sink, Stream}; use http::Uri; use itertools::Itertools; use keys::hash::H256; -use keys::{Address, Type as ScriptType}; +use keys::Address; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, BigInt, MmNumber}; use mm2_rpc::data::legacy::ElectrumProtocol; @@ -317,6 +317,10 @@ impl From for UtxoRpcError { fn from(e: NumConversError) -> Self { UtxoRpcError::Internal(e.to_string()) } } +impl From for UtxoRpcError { + fn from(e: keys::Error) -> Self { UtxoRpcError::Internal(e.to_string()) } +} + impl UtxoRpcError { pub fn is_tx_not_found_error(&self) -> bool { if let UtxoRpcError::ResponseParseError(ref json_err) = self { @@ -2216,7 +2220,7 @@ impl ElectrumClient { #[cfg_attr(test, mockable)] impl UtxoRpcClientOps for ElectrumClient { fn list_unspent(&self, address: &Address, _decimals: u8) -> UtxoRpcFut> { - let script = output_script(address, ScriptType::P2PKH); + let script = try_f!(output_script(address)); let script_hash = electrum_script_hash(&script); Box::new( self.scripthash_list_unspent(&hex::encode(script_hash)) @@ -2238,14 +2242,14 @@ impl UtxoRpcClientOps for ElectrumClient { } fn list_unspent_group(&self, addresses: Vec
, _decimals: u8) -> UtxoRpcFut { - let script_hashes = addresses + let script_hashes = try_f!(addresses .iter() .map(|addr| { - let script = output_script(addr, ScriptType::P2PKH); + let script = output_script(addr)?; let script_hash = electrum_script_hash(&script); - hex::encode(script_hash) + Ok(hex::encode(script_hash)) }) - .collect(); + .collect::, keys::Error>>()); let this = self.clone(); let fut = async move { @@ -2320,7 +2324,12 @@ impl UtxoRpcClientOps for ElectrumClient { } fn display_balance(&self, address: Address, decimals: u8) -> RpcRes { - let hash = electrum_script_hash(&output_script(&address, ScriptType::P2PKH)); + let output_script = try_f!(output_script(&address).map_err(|err| JsonRpcError::new( + UtxoJsonRpcClientInfo::client_info(self), + rpc_req!(self, "blockchain.scripthash.get_balance").into(), + JsonRpcErrorType::Internal(err.to_string()) + ))); + let hash = electrum_script_hash(&output_script); let hash_str = hex::encode(hash); Box::new( self.scripthash_get_balance(&hash_str) @@ -2331,10 +2340,15 @@ impl UtxoRpcClientOps for ElectrumClient { fn display_balances(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut> { let this = self.clone(); let fut = async move { - let hashes = addresses.iter().map(|address| { - let hash = electrum_script_hash(&output_script(address, ScriptType::P2PKH)); - hex::encode(hash) - }); + let hashes = addresses + .iter() + .map(|address| { + let output_script = output_script(address)?; + let hash = electrum_script_hash(&output_script); + + Ok(hex::encode(hash)) + }) + .collect::, keys::Error>>()?; let electrum_balances = this.scripthash_get_balances(hashes).compat().await?; let balances = electrum_balances diff --git a/mm2src/coins/utxo/utxo_balance_events.rs b/mm2src/coins/utxo/utxo_balance_events.rs index 7ff6957c3b..b620a2ef9e 100644 --- a/mm2src/coins/utxo/utxo_balance_events.rs +++ b/mm2src/coins/utxo/utxo_balance_events.rs @@ -46,7 +46,7 @@ impl EventBehaviour for UtxoStandardCoin { let mut scripthash_to_address_map: BTreeMap = BTreeMap::new(); for address in addresses { - let scripthash = address_to_scripthash(&address); + let scripthash = address_to_scripthash(&address).map_err(|e| e.to_string())?; scripthash_to_address_map.insert(scripthash.clone(), address); @@ -129,7 +129,13 @@ impl EventBehaviour for UtxoStandardCoin { None => try_or_continue!(self.my_addresses().await) .into_iter() .find_map(|addr| { - let script = output_script(&addr, keys::Type::P2PKH); + let script = match output_script(&addr) { + Ok(script) => script, + Err(e) => { + log::error!("{e}"); + return None; + }, + }; let script_hash = electrum_script_hash(&script); let scripthash = hex::encode(script_hash); diff --git a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs index 3c1fc84e9c..60b4d75ff0 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs @@ -181,7 +181,7 @@ async fn merge_utxo_loop( let unspents: Vec<_> = unspents.into_iter().take(max_merge_at_once).collect(); info!("Trying to merge {} UTXOs of coin {}", unspents.len(), ticker); let value = unspents.iter().fold(0, |sum, unspent| sum + unspent.value); - let script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); + let script_pubkey = Builder::build_p2pkh(my_address.hash()).to_bytes(); let output = TransactionOutput { value, script_pubkey }; let merge_tx_fut = generate_and_send_tx( &coin, diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index b3c2d19680..6f10e9d791 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -26,8 +26,8 @@ use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures::StreamExt; use keys::bytes::Bytes; -pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, - Type as ScriptType}; +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 primitives::hash::H160; @@ -127,6 +127,10 @@ impl From for UtxoCoinBuildError { fn from(e: PrivKeyPolicyNotAllowed) -> Self { UtxoCoinBuildError::PrivKeyPolicyNotAllowed(e) } } +impl From for UtxoCoinBuildError { + fn from(e: keys::Error) -> Self { UtxoCoinBuildError::Internal(e.to_string()) } +} + #[async_trait] pub trait UtxoCoinBuilder: UtxoFieldsWithIguanaSecretBuilder + UtxoFieldsWithGlobalHDBuilder + UtxoFieldsWithHardwareWalletBuilder @@ -229,16 +233,18 @@ where { let key_pair = priv_key_policy.activated_key_or_err()?; let addr_format = builder.address_format()?; - let my_address = Address { - prefix: conf.pub_addr_prefix, - t_addr_prefix: conf.pub_t_addr_prefix, - hash: AddressHashEnum::AddressHash(key_pair.public().address_hash()), - checksum_type: conf.checksum_type, - hrp: conf.bech32_hrp.clone(), + let my_address = AddressBuilder::new( addr_format, - }; - - let my_script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + AddressHashEnum::AddressHash(key_pair.public().address_hash()), + conf.checksum_type, + conf.address_prefixes.clone(), + conf.bech32_hrp.clone(), + ) + .as_pkh() + .build() + .map_to_mm(UtxoCoinBuildError::Internal)?; + + let my_script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let derivation_method = DerivationMethod::SingleAddress(my_address); let (scripthash_notification_sender, scripthash_notification_handler) = diff --git a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs index a950b67cb4..a154a7135b 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs @@ -5,12 +5,14 @@ use crate::UtxoActivationParams; use bitcrypto::ChecksumType; use crypto::{Bip32Error, StandardHDPathToCoin}; use derive_more::Display; -pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, - Type as ScriptType}; +use keys::NetworkAddressPrefixes; +pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressScriptType, KeyPair, Private, + Public, Secret}; use mm2_err_handle::prelude::*; use script::SignatureVersion; use serde_json::{self as json, Value as Json}; use spv_validation::conf::SPVConf; +use std::convert::TryInto; use std::num::NonZeroU64; use std::sync::atomic::AtomicBool; @@ -51,10 +53,29 @@ impl<'a> UtxoConfBuilder<'a> { pub fn build(&self) -> UtxoConfResult { let checksum_type = self.checksum_type(); + let pub_addr_prefix = self.pub_addr_prefix(); - let p2sh_addr_prefix = self.p2sh_address_prefix(); let pub_t_addr_prefix = self.pub_t_address_prefix(); + let mut p2pkh_prefixes = vec![]; + if pub_t_addr_prefix != 0 { + p2pkh_prefixes.push(pub_t_addr_prefix); + } + p2pkh_prefixes.push(pub_addr_prefix); + drop_mutability!(p2pkh_prefixes); + + let p2sh_addr_prefix = self.p2sh_address_prefix(); let p2sh_t_addr_prefix = self.p2sh_t_address_prefix(); + let mut p2sh_prefixes = vec![]; + if p2sh_t_addr_prefix != 0 { + p2sh_prefixes.push(p2sh_t_addr_prefix); + } + p2sh_prefixes.push(p2sh_addr_prefix); + drop_mutability!(p2sh_prefixes); + + let address_prefixes = NetworkAddressPrefixes { + p2pkh: p2pkh_prefixes.as_slice().try_into().expect("prefixes valid"), + p2sh: p2sh_prefixes.as_slice().try_into().expect("prefixes valid"), + }; let sign_message_prefix = self.sign_message_prefix(); let wif_prefix = self.wif_prefix(); @@ -99,10 +120,7 @@ impl<'a> UtxoConfBuilder<'a> { is_posv, requires_notarization, overwintered, - pub_addr_prefix, - p2sh_addr_prefix, - pub_t_addr_prefix, - p2sh_t_addr_prefix, + address_prefixes, sign_message_prefix, bech32_hrp, segwit, diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index c0f08f5dae..ddec247769 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -44,8 +44,9 @@ use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Either; use itertools::Itertools; use keys::bytes::Bytes; -use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, CompactSignature, Public, SegwitAddress, - Type as ScriptType}; +#[cfg(test)] use keys::prefixes::{KMD_PREFIXES, T_QTUM_PREFIXES}; +use keys::{Address, AddressBuilder, AddressBuilderOption, AddressFormat as UtxoAddressFormat, AddressHashEnum, + AddressScriptType, CompactSignature, Public, SegwitAddress}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::bigdecimal_custom::CheckedDivision; @@ -628,29 +629,29 @@ pub fn addresses_from_script(coin: &T, script: &Script) -> Res let addresses = destinations .into_iter() .map(|dst| { - let (prefix, t_addr_prefix, addr_format) = match dst.kind { - ScriptType::P2PKH => ( - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + let (addr_format, build_option) = match dst.kind { + AddressScriptType::P2PKH => ( coin.addr_format_for_standard_scripts(), + AddressBuilderOption::BuildAsPubkeyHash, ), - ScriptType::P2SH => ( - conf.p2sh_addr_prefix, - conf.p2sh_t_addr_prefix, + AddressScriptType::P2SH => ( coin.addr_format_for_standard_scripts(), + AddressBuilderOption::BuildAsScriptHash, ), - ScriptType::P2WPKH => (conf.pub_addr_prefix, conf.pub_t_addr_prefix, UtxoAddressFormat::Segwit), - ScriptType::P2WSH => (conf.pub_addr_prefix, conf.pub_t_addr_prefix, UtxoAddressFormat::Segwit), + AddressScriptType::P2WPKH => (UtxoAddressFormat::Segwit, AddressBuilderOption::BuildAsPubkeyHash), + AddressScriptType::P2WSH => (UtxoAddressFormat::Segwit, AddressBuilderOption::BuildAsScriptHash), }; - Address { - hash: dst.hash, - checksum_type: conf.checksum_type, - prefix, - t_addr_prefix, - hrp: conf.bech32_hrp.clone(), + AddressBuilder::new( addr_format, - } + dst.hash, + conf.checksum_type, + conf.address_prefixes.clone(), + conf.bech32_hrp.clone(), + ) + .with_build_option(build_option) + .build() + .expect("valid address props") }) .collect(); @@ -671,28 +672,17 @@ where pub fn address_from_str_unchecked(coin: &UtxoCoinFields, address: &str) -> MmResult { let mut errors = Vec::with_capacity(3); - match Address::from_str(address) { + match Address::from_legacyaddress(address, &coin.conf.address_prefixes) { Ok(legacy) => return Ok(legacy), - Err(e) => errors.push(e.to_string()), + Err(e) => errors.push(e), }; - match Address::from_segwitaddress( - address, - coin.conf.checksum_type, - coin.conf.pub_addr_prefix, - coin.conf.pub_t_addr_prefix, - ) { + match Address::from_segwitaddress(address, coin.conf.checksum_type) { Ok(segwit) => return Ok(segwit), Err(e) => errors.push(e), } - match Address::from_cashaddress( - address, - coin.conf.checksum_type, - coin.conf.pub_addr_prefix, - coin.conf.p2sh_addr_prefix, - coin.conf.pub_t_addr_prefix, - ) { + match Address::from_cashaddress(address, coin.conf.checksum_type, &coin.conf.address_prefixes) { Ok(cashaddress) => return Ok(cashaddress), Err(e) => errors.push(e), } @@ -756,33 +746,43 @@ pub fn tx_size_in_v_bytes(from_addr_format: &UtxoAddressFormat, tx: &UtxoTx) -> } } -/// Implements building utxo script pubkey for an address by the address format -pub fn get_script_for_address(coin: &UtxoCoinFields, addr: &Address) -> MmResult { - match addr.addr_format { +/// Implements building utxo script pubkey for an address with checking coin conf prefixes +pub fn output_script_checked(coin: &UtxoCoinFields, addr: &Address) -> MmResult { + match addr.addr_format() { UtxoAddressFormat::Standard => { - if addr.prefix == coin.conf.pub_addr_prefix && addr.t_addr_prefix == coin.conf.pub_t_addr_prefix { - Ok(Builder::build_p2pkh(&addr.hash)) - } else if addr.prefix == coin.conf.p2sh_addr_prefix && addr.t_addr_prefix == coin.conf.p2sh_t_addr_prefix { - Ok(Builder::build_p2sh(&addr.hash)) - } else { - MmError::err(UnsupportedAddr::PrefixError(coin.conf.ticker.clone())) + if addr.prefix() != &coin.conf.address_prefixes.p2pkh && addr.prefix() != &coin.conf.address_prefixes.p2sh { + return MmError::err(UnsupportedAddr::PrefixError(coin.conf.ticker.clone())); } }, - UtxoAddressFormat::Segwit => Ok(Builder::build_p2witness(&addr.hash)), + UtxoAddressFormat::Segwit => match (coin.conf.bech32_hrp.as_ref(), addr.hrp().as_ref()) { + (Some(conf_hrp), Some(addr_hrp)) => { + if conf_hrp != addr_hrp { + return MmError::err(UnsupportedAddr::HrpError { + ticker: coin.conf.ticker.clone(), + hrp: addr_hrp.to_string(), + }); + } + }, + (_, _) => { + return MmError::err(UnsupportedAddr::HrpError { + ticker: coin.conf.ticker.clone(), + hrp: addr.hrp().clone().unwrap_or_else(|| "".to_owned()), + }); + }, + }, UtxoAddressFormat::CashAddress { network: _, pub_addr_prefix, p2sh_addr_prefix, } => { - if pub_addr_prefix == coin.conf.pub_addr_prefix { - Ok(Builder::build_p2pkh(&addr.hash)) - } else if p2sh_addr_prefix == coin.conf.p2sh_addr_prefix { - Ok(Builder::build_p2sh(&addr.hash)) - } else { - MmError::err(UnsupportedAddr::PrefixError(coin.conf.ticker.clone())) + if AddressPrefix::from([*pub_addr_prefix]) != coin.conf.address_prefixes.p2pkh + && AddressPrefix::from([*p2sh_addr_prefix]) != coin.conf.address_prefixes.p2sh + { + return MmError::err(UnsupportedAddr::PrefixError(coin.conf.ticker.clone())); } }, } + output_script(addr).map_to_mm(UnsupportedAddr::from) } pub struct UtxoTxBuilder<'a, T: AsRef + UtxoTxGenerationOps> { @@ -959,12 +959,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { .from .clone() .or_mm_err(|| GenerateTxError::Internal("'from' address is not specified".to_owned()))?; - let change_dest_type = if from.addr_format == UtxoAddressFormat::Segwit { - ScriptType::P2WPKH - } else { - ScriptType::P2PKH - }; - let change_script_pubkey = output_script(&from, change_dest_type).to_bytes(); + let change_script_pubkey = output_script(&from).map(|script| script.to_bytes())?; let actual_tx_fee = match self.fee { Some(fee) => fee, @@ -1016,7 +1011,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { }); self.sum_inputs += utxo.value; - if self.update_fee_and_check_completeness(&from.addr_format, &actual_tx_fee) { + if self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { break; } } @@ -1537,14 +1532,16 @@ pub async fn sign_and_send_taker_funding_spend( gen_args.taker_pub, gen_args.maker_pub, ); - let payment_address = Address { - checksum_type: coin.as_ref().conf.checksum_type, - hash: AddressHashEnum::AddressHash(dhash160(&payment_redeem_script)), - prefix: coin.as_ref().conf.p2sh_addr_prefix, - t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Standard, - }; + let payment_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + AddressHashEnum::AddressHash(dhash160(&payment_redeem_script)), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh() + .build() + .map_err(TransactionErr::Plain)?; let payment_address_str = payment_address.to_string(); try_tx_s!( client @@ -1567,8 +1564,7 @@ async fn gen_taker_payment_spend_preimage( let dex_fee_address = address_from_raw_pubkey( args.dex_fee_pub, - coin.as_ref().conf.pub_addr_prefix, - coin.as_ref().conf.pub_t_addr_prefix, + coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), coin.addr_format().clone(), @@ -1576,7 +1572,7 @@ async fn gen_taker_payment_spend_preimage( .map_to_mm(|e| TxGenError::AddressDerivation(format!("Failed to derive dex_fee_address: {}", e)))?; let dex_fee_output = TransactionOutput { value: dex_fee_sat, - script_pubkey: Builder::build_p2pkh(&dex_fee_address.hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(dex_fee_address.hash()).to_bytes(), }; p2sh_spending_tx_preimage( @@ -1702,9 +1698,10 @@ pub async fn sign_and_broadcast_taker_payment_spend( } let maker_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()); + let script_pubkey = output_script(maker_address).map(|script| script.to_bytes())?; let maker_output = TransactionOutput { value: maker_sat - miner_fee, - script_pubkey: output_script(maker_address, ScriptType::P2PKH).to_bytes(), + script_pubkey, }; signer.outputs.push(maker_output); drop_mutability!(signer); @@ -1750,8 +1747,7 @@ where { let address = try_tx_fus!(address_from_raw_pubkey( fee_pub_key, - coin.as_ref().conf.pub_addr_prefix, - coin.as_ref().conf.pub_t_addr_prefix, + coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), coin.addr_format().clone(), @@ -1759,7 +1755,7 @@ where let outputs = try_tx_fus!(generate_taker_fee_tx_outputs( coin.as_ref().decimals, - &address.hash, + address.hash(), dex_fee, )); @@ -1892,7 +1888,7 @@ pub fn send_maker_spends_taker_payment(coin: T, args payment_value ); } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { value: payment_value - fee, script_pubkey, @@ -1998,7 +1994,7 @@ pub fn create_maker_payment_spend_preimage( payment_value ); } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { value: payment_value - fee, script_pubkey, @@ -2057,7 +2053,7 @@ pub fn create_taker_payment_refund_preimage( payment_value ); } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { value: payment_value - fee, script_pubkey, @@ -2114,7 +2110,7 @@ pub fn send_taker_spends_maker_payment(coin: T, args payment_value ); } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { value: payment_value - fee, script_pubkey, @@ -2180,7 +2176,7 @@ async fn refund_htlc_payment( payment_value ); } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { value: payment_value - fee, script_pubkey, @@ -2392,8 +2388,7 @@ pub fn watcher_validate_taker_fee( let address = address_from_raw_pubkey( &fee_addr, - coin.as_ref().conf.pub_addr_prefix, - coin.as_ref().conf.pub_t_addr_prefix, + coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), coin.addr_format().clone(), @@ -2402,7 +2397,7 @@ pub fn watcher_validate_taker_fee( match taker_fee_tx.outputs.get(output_index) { Some(out) => { - let expected_script_pubkey = Builder::build_p2pkh(&address.hash).to_bytes(); + let expected_script_pubkey = Builder::build_p2pkh(address.hash()).to_bytes(); if out.script_pubkey != expected_script_pubkey { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", @@ -2435,8 +2430,7 @@ pub fn validate_fee( ) -> ValidatePaymentFut<()> { let address = try_f!(address_from_raw_pubkey( fee_addr, - coin.as_ref().conf.pub_addr_prefix, - coin.as_ref().conf.pub_t_addr_prefix, + coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), coin.addr_format().clone(), @@ -2485,7 +2479,7 @@ pub fn validate_fee( match tx.outputs.get(output_index) { Some(out) => { - let expected_script_pubkey = Builder::build_p2pkh(&address.hash).to_bytes(); + let expected_script_pubkey = Builder::build_p2pkh(address.hash()).to_bytes(); if out.script_pubkey != expected_script_pubkey { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", @@ -2685,13 +2679,13 @@ pub fn validate_payment_spend_or_refund( payment_spend_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; let my_address = try_f!(coin.as_ref().derivation_method.single_addr_or_err()); - let expected_script_pubkey = &output_script(my_address, ScriptType::P2PKH).to_bytes(); + let expected_script_pubkey = try_f!(output_script(my_address).map(|script| script.to_bytes())); let output = try_f!(payment_spend_tx .outputs .get(DEFAULT_SWAP_VOUT) .ok_or_else(|| ValidatePaymentError::WrongPaymentTx("Payment tx has no outputs".to_string(),))); - if expected_script_pubkey != &output.script_pubkey { + if expected_script_pubkey != output.script_pubkey { return Box::new(futures01::future::err( ValidatePaymentError::WrongPaymentTx(format!( "Provided payment tx script pubkey doesn't match expected {:?} {:?}", @@ -2736,14 +2730,15 @@ pub fn check_if_my_payment_sent( } }, UtxoRpcClientEnum::Native(client) => { - let target_addr = Address { - t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - prefix: coin.as_ref().conf.p2sh_addr_prefix, - hash: hash.into(), - checksum_type: coin.as_ref().conf.checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: coin.addr_format().clone(), - }; + let target_addr = AddressBuilder::new( + coin.addr_format_for_standard_scripts(), + hash.into(), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh() + .build()?; let target_addr = target_addr.to_string(); let is_imported = try_s!(client.is_address_imported(&target_addr).await); if !is_imported { @@ -2937,7 +2932,7 @@ pub fn verify_message( let signature = CompactSignature::from(base64::decode(signature_base64)?); let recovered_pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; let received_address = checked_address_from_str(coin, address)?; - Ok(AddressHashEnum::from(recovered_pubkey.address_hash()) == received_address.hash) + Ok(AddressHashEnum::from(recovered_pubkey.address_hash()) == *received_address.hash()) } pub fn my_balance(coin: T) -> BalanceFut @@ -3415,25 +3410,26 @@ pub fn decimals(coin: &UtxoCoinFields) -> u8 { coin.decimals } pub fn convert_to_address(coin: &T, from: &str, to_address_format: Json) -> Result { let to_address_format: UtxoAddressFormat = json::from_value(to_address_format).map_err(|e| ERRL!("Error on parse UTXO address format {:?}", e))?; - let mut from_address = try_s!(coin.address_from_str(from)); + let from_address = try_s!(coin.address_from_str(from)); match to_address_format { UtxoAddressFormat::Standard => { - from_address.addr_format = UtxoAddressFormat::Standard; - Ok(from_address.to_string()) + // assuming convertion to p2pkh + Ok(LegacyAddress::new( + from_address.hash(), + coin.as_ref().conf.address_prefixes.p2pkh.clone(), + coin.as_ref().conf.checksum_type, + ) + .to_string()) }, UtxoAddressFormat::Segwit => { let bech32_hrp = &coin.as_ref().conf.bech32_hrp; match bech32_hrp { - Some(hrp) => Ok(SegwitAddress::new(&from_address.hash, hrp.clone()).to_string()), + Some(hrp) => Ok(SegwitAddress::new(from_address.hash(), hrp.clone()).to_string()), None => ERR!("Cannot convert to a segwit address for a coin with no bech32_hrp in config"), } }, UtxoAddressFormat::CashAddress { network, .. } => Ok(try_s!(from_address - .to_cashaddress( - &network, - coin.as_ref().conf.pub_addr_prefix, - coin.as_ref().conf.p2sh_addr_prefix - ) + .to_cashaddress(&network, &coin.as_ref().conf.address_prefixes) .and_then(|cashaddress| cashaddress.encode()))), } } @@ -3450,11 +3446,10 @@ pub fn validate_address(coin: &T, address: &str) -> ValidateAd }, }; - let is_p2pkh = address.prefix == coin.as_ref().conf.pub_addr_prefix - && address.t_addr_prefix == coin.as_ref().conf.pub_t_addr_prefix; - let is_p2sh = address.prefix == coin.as_ref().conf.p2sh_addr_prefix - && address.t_addr_prefix == coin.as_ref().conf.p2sh_t_addr_prefix; - let is_segwit = address.hrp.is_some() && address.hrp == coin.as_ref().conf.bech32_hrp && coin.as_ref().conf.segwit; + let is_p2pkh = address.prefix() == &coin.as_ref().conf.address_prefixes.p2pkh; + let is_p2sh = address.prefix() == &coin.as_ref().conf.address_prefixes.p2sh; + let is_segwit = + address.hrp().is_some() && address.hrp() == &coin.as_ref().conf.bech32_hrp && coin.as_ref().conf.segwit; if is_p2pkh || is_p2sh || is_segwit { ValidateAddressResult { @@ -3464,7 +3459,7 @@ pub fn validate_address(coin: &T, address: &str) -> ValidateAd } else { ValidateAddressResult { is_valid: false, - reason: Some(ERRL!("Address {} has invalid prefixes", address)), + reason: Some(ERRL!("Address {} has invalid prefix", address)), } } } @@ -3832,7 +3827,10 @@ where Ok(my_address) => my_address, Err(e) => return RequestTxHistoryResult::CriticalError(e.to_string()), }; - let script = output_script(my_address, ScriptType::P2PKH); + let script = match output_script(my_address) { + Ok(script) => script, + Err(err) => return RequestTxHistoryResult::CriticalError(err.to_string()), + }; let script_hash = electrum_script_hash(&script); mm_counter!(metrics, "tx.history.request.count", 1, @@ -4577,38 +4575,33 @@ pub fn big_decimal_from_sat_unsigned(satoshis: u64, decimals: u8) -> BigDecimal pub fn address_from_raw_pubkey( pub_key: &[u8], - prefix: u8, - t_addr_prefix: u8, + prefixes: NetworkAddressPrefixes, checksum_type: ChecksumType, hrp: Option, addr_format: UtxoAddressFormat, ) -> Result { - Ok(Address { - t_addr_prefix, - prefix, - hash: try_s!(Public::from_slice(pub_key)).address_hash().into(), + AddressBuilder::new( + addr_format, + try_s!(Public::from_slice(pub_key)).address_hash().into(), checksum_type, + prefixes, hrp, - addr_format, - }) + ) + .as_pkh() + .build() } pub fn address_from_pubkey( pub_key: &Public, - prefix: u8, - t_addr_prefix: u8, + prefixes: NetworkAddressPrefixes, checksum_type: ChecksumType, hrp: Option, addr_format: UtxoAddressFormat, ) -> Address { - Address { - t_addr_prefix, - prefix, - hash: pub_key.address_hash().into(), - checksum_type, - hrp, - addr_format, - } + AddressBuilder::new(addr_format, pub_key.address_hash().into(), checksum_type, prefixes, hrp) + .as_pkh() + .build() + .expect("valid address props") } #[allow(clippy::too_many_arguments)] @@ -4821,14 +4814,15 @@ where script_pubkey: op_return_script, }; - let payment_address = Address { - checksum_type: coin.as_ref().conf.checksum_type, - hash: redeem_script_hash.into(), - prefix: coin.as_ref().conf.p2sh_addr_prefix, - t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Standard, - }; + let payment_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + redeem_script_hash.into(), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh() + .build()?; let result = SwapPaymentOutputsResult { payment_address, outputs: vec![htlc_out, op_return_out], @@ -5035,7 +5029,7 @@ where pub fn addr_format(coin: &dyn AsRef) -> &UtxoAddressFormat { match coin.as_ref().derivation_method { - DerivationMethod::SingleAddress(ref my_address) => &my_address.addr_format, + DerivationMethod::SingleAddress(ref my_address) => my_address.addr_format(), DerivationMethod::HDWallet(UtxoHDWallet { ref address_format, .. }) => address_format, } } @@ -5053,12 +5047,12 @@ where { let conf = &coin.as_ref().conf; - match addr.addr_format { + match addr.addr_format() { // Considering that legacy is supported with any configured formats // This can be changed depending on the coins implementation UtxoAddressFormat::Standard => { - let is_p2pkh = addr.prefix == conf.pub_addr_prefix && addr.t_addr_prefix == conf.pub_t_addr_prefix; - let is_p2sh = addr.prefix == conf.p2sh_addr_prefix && addr.t_addr_prefix == conf.p2sh_t_addr_prefix; + let is_p2pkh = addr.prefix() == &conf.address_prefixes.p2pkh; + let is_p2sh = addr.prefix() == &conf.address_prefixes.p2sh; if !is_p2pkh && !is_p2sh { MmError::err(UnsupportedAddr::PrefixError(conf.ticker.clone())) } else { @@ -5070,23 +5064,23 @@ where return MmError::err(UnsupportedAddr::SegwitNotActivated(conf.ticker.clone())); } - if addr.hrp != conf.bech32_hrp { + if addr.hrp() != &conf.bech32_hrp { MmError::err(UnsupportedAddr::HrpError { ticker: conf.ticker.clone(), - hrp: addr.hrp.clone().unwrap_or_default(), + hrp: addr.hrp().clone().unwrap_or_default(), }) } else { Ok(()) } }, UtxoAddressFormat::CashAddress { .. } => { - if addr.addr_format == conf.default_address_format || addr.addr_format == *coin.addr_format() { + if addr.addr_format() == &conf.default_address_format || addr.addr_format() == coin.addr_format() { Ok(()) } else { MmError::err(UnsupportedAddr::FormatMismatch { ticker: conf.ticker.clone(), activated_format: coin.addr_format().to_string(), - used_format: addr.addr_format.to_string(), + used_format: addr.addr_format().to_string(), }) } }, @@ -5226,7 +5220,7 @@ where payment_value ); } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { value: payment_value - fee, script_pubkey, @@ -5307,10 +5301,10 @@ where refund_htlc_payment(coin, args, SwapPaymentType::TakerPaymentV2).await } -pub fn address_to_scripthash(address: &Address) -> String { - let script = output_script(address, keys::Type::P2PKH); +pub fn address_to_scripthash(address: &Address) -> Result { + let script = output_script(address)?; let script_hash = electrum_script_hash(&script); - hex::encode(script_hash) + Ok(hex::encode(script_hash)) } pub async fn utxo_prepare_addresses_for_balance_stream_if_enabled( @@ -5434,18 +5428,18 @@ fn test_generate_taker_fee_tx_outputs_with_burn() { #[test] fn test_address_to_scripthash() { - let address = Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk"); - let actual = address_to_scripthash(&address); + let address = Address::from_legacyaddress("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk", &KMD_PREFIXES).unwrap(); + let actual = address_to_scripthash(&address).expect("valid script hash to be built"); let expected = "e850499408c6ebcf6b3340282747e540fb23748429fca5f2b36cdeef54ddf5b1".to_owned(); assert_eq!(expected, actual); - let address = Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"); - let actual = address_to_scripthash(&address); + let address = Address::from_legacyaddress("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", &KMD_PREFIXES).unwrap(); + let actual = address_to_scripthash(&address).expect("valid script hash to be built"); let expected = "a70a7a7041ef172ce4b5f8208aabed44c81e2af75493540f50af7bd9afa9955d".to_owned(); assert_eq!(expected, actual); - let address = Address::from("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); - let actual = address_to_scripthash(&address); + let address = Address::from_legacyaddress("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", &T_QTUM_PREFIXES).unwrap(); + let actual = address_to_scripthash(&address).expect("valid script hash to be built"); let expected = "c5b5922c86830289231539d1681d8ce621aac8326c96d6ac55400b4d1485f769".to_owned(); assert_eq!(expected, actual); } diff --git a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs index 97a637a68c..3b4e7959e1 100644 --- a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs +++ b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs @@ -15,7 +15,7 @@ use common::jsonrpc_client::JsonRpcErrorType; use crypto::Bip44Chain; use futures::compat::Future01CompatExt; use itertools::Itertools; -use keys::{Address, Type as ScriptType}; +use keys::Address; use mm2_err_handle::prelude::*; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; @@ -365,14 +365,18 @@ async fn request_tx_history_with_electrum( metrics: MetricsArc, for_addresses: &HashSet
, ) -> RequestTxHistoryResult { - fn addr_to_script_hash(addr: &Address) -> String { - let script = output_script(addr, ScriptType::P2PKH); + fn addr_to_script_hash(addr: &Address) -> Result { + let script = output_script(addr)?; let script_hash = electrum_script_hash(&script); - hex::encode(script_hash) + Ok(hex::encode(script_hash)) } let script_hashes_count = for_addresses.len() as u64; - let script_hashes = for_addresses.iter().map(addr_to_script_hash); + let script_hashes: Result, _> = for_addresses.iter().map(addr_to_script_hash).collect(); + let script_hashes = match script_hashes { + Ok(script_hashes) => script_hashes, + Err(err) => return RequestTxHistoryResult::CriticalError(err.to_string()), + }; mm_counter!(metrics, "tx.history.request.count", script_hashes_count, "coin" => ticker, "client" => "electrum", "method" => "blockchain.scripthash.get_history"); diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index f4d9adc2c6..4a716182a7 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -14,7 +14,9 @@ use common::jsonrpc_client::JsonRpcErrorType; use common::PagingOptionsEnum; use crypto::privkey::key_pair_from_seed; use itertools::Itertools; +use keys::prefixes::*; use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; +use std::convert::TryFrom; use std::num::NonZeroUsize; use std::time::Duration; @@ -61,23 +63,35 @@ pub(super) fn utxo_coin_fields_for_test( }, }; let key_pair = key_pair_from_seed(&seed).unwrap(); - let my_address = Address { - prefix: 60, - hash: key_pair.public().address_hash().into(), - t_addr_prefix: 0, - checksum_type, - hrp: if is_segwit_coin { - Some(TEST_COIN_HRP.to_string()) - } else { - None - }, - addr_format: if is_segwit_coin { - UtxoAddressFormat::Segwit - } else { - UtxoAddressFormat::Standard - }, + let prefixes = if is_segwit_coin { + NetworkAddressPrefixes::default() + } else { + NetworkAddressPrefixes { + p2pkh: [60].into(), + p2sh: AddressPrefix::default(), + } }; - let my_script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); + let hrp = if is_segwit_coin { + Some(TEST_COIN_HRP.to_string()) + } else { + None + }; + let addr_format = if is_segwit_coin { + UtxoAddressFormat::Segwit + } else { + UtxoAddressFormat::Standard + }; + let my_address = AddressBuilder::new( + addr_format, + key_pair.public().address_hash().into(), + checksum_type, + prefixes, + hrp, + ) + .as_pkh() + .build() + .expect("valid address props"); + let my_script_pubkey = Builder::build_p2pkh(my_address.hash()).to_bytes(); let priv_key_policy = PrivKeyPolicy::Iguana(key_pair); let derivation_method = DerivationMethod::SingleAddress(my_address); @@ -98,10 +112,10 @@ pub(super) fn utxo_coin_fields_for_test( tx_version: 4, default_address_format: UtxoAddressFormat::Standard, asset_chain: true, - p2sh_addr_prefix: 85, - p2sh_t_addr_prefix: 0, - pub_addr_prefix: 60, - pub_t_addr_prefix: 0, + address_prefixes: NetworkAddressPrefixes { + p2pkh: [60].into(), + p2sh: [85].into(), + }, sign_message_prefix: Some(String::from("Komodo Signed Message:\n")), bech32_hrp, ticker: TEST_COIN_NAME.into(), @@ -196,29 +210,29 @@ pub(super) fn get_morty_hd_transactions_ordered(tx_hashes: &[&str]) -> Vec = vec![ ( - "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), - BigDecimal::from_str("5.77699").unwrap(), + Address::from_legacyaddress("RG278CfeNPFtNztFZQir8cgdWexVhViYVy", &KMD_PREFIXES).unwrap(), + BigDecimal::try_from(5.77699).unwrap(), ), ( - "RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), - BigDecimal::from_str("3.33").unwrap(), + Address::from_legacyaddress("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN", &KMD_PREFIXES).unwrap(), + BigDecimal::try_from(3.33).unwrap(), ), ( - "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), - BigDecimal::from_str("0.77699").unwrap(), + Address::from_legacyaddress("RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi", &KMD_PREFIXES).unwrap(), + BigDecimal::try_from(0.77699).unwrap(), ), ( - "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), - BigDecimal::from_str("16.55398").unwrap(), + Address::from_legacyaddress("RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF", &KMD_PREFIXES).unwrap(), + BigDecimal::try_from(16.55398).unwrap(), ), ]; assert_eq!(actual, expected); diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 77d6855328..0853563f11 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -194,7 +194,7 @@ impl UtxoCommonOps for UtxoStandardCoin { } fn script_for_address(&self, address: &Address) -> MmResult { - utxo_common::get_script_for_address(self.as_ref(), address) + utxo_common::output_script_checked(self.as_ref(), address) } async fn get_current_mtp(&self) -> UtxoRpcResult { @@ -266,8 +266,7 @@ impl UtxoCommonOps for UtxoStandardCoin { let conf = &self.utxo_arc.conf; utxo_common::address_from_pubkey( pubkey, - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + conf.address_prefixes.clone(), conf.checksum_type, conf.bech32_hrp.clone(), self.addr_format().clone(), diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 3e4193c375..3cd2090f1a 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -40,6 +40,7 @@ use db_common::sqlite::rusqlite::Connection; use futures::channel::mpsc::channel; use futures::future::join_all; use futures::TryFutureExt; +use keys::prefixes::*; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_number::bigdecimal::{BigDecimal, Signed}; use mm2_test_helpers::electrums::doc_electrums; @@ -239,7 +240,7 @@ fn test_generate_transaction() { }]; let outputs = vec![TransactionOutput { - script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_single_addr().hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(coin.as_ref().derivation_method.unwrap_single_addr().hash()).to_bytes(), value: 100000, }]; @@ -283,13 +284,21 @@ fn test_addresses_from_script() { let coin = utxo_coin_for_test(client.into(), None, false); // P2PKH let script: Script = "76a91405aab5342166f8594baf17a7d9bef5d56744332788ac".into(); - let expected_addr: Vec
= vec!["R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW".into()]; + let expected_addr: Vec
= vec![Address::from_legacyaddress( + "R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap()]; let actual_addr = coin.addresses_from_script(&script).unwrap(); assert_eq!(expected_addr, actual_addr); // P2SH let script: Script = "a914e71a6120653ebd526e0f9d7a29cde5969db362d487".into(); - let expected_addr: Vec
= vec!["bZoEPR7DjTqSDiQTeRFNDJuQPTRY2335LD".into()]; + let expected_addr: Vec
= vec![Address::from_legacyaddress( + "bZoEPR7DjTqSDiQTeRFNDJuQPTRY2335LD", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap()]; let actual_addr = coin.addresses_from_script(&script).unwrap(); assert_eq!(expected_addr, actual_addr); } @@ -960,7 +969,7 @@ fn test_utxo_lock() { let coin = utxo_coin_for_test(client.into(), None, false); let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_single_addr().hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(coin.as_ref().derivation_method.unwrap_single_addr().hash()).to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -1536,7 +1545,7 @@ fn test_spam_rick() { let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_single_addr().hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(coin.as_ref().derivation_method.unwrap_single_addr().hash()).to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -1603,8 +1612,12 @@ fn test_qtum_generate_pod() { let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); let coin = block_on(qtum_coin_with_priv_key(&ctx, "tQTUM", &conf, ¶ms, priv_key)).unwrap(); let expected_res = "20086d757b34c01deacfef97a391f8ed2ca761c72a08d5000adc3d187b1007aca86a03bc5131b1f99b66873a12b51f8603213cdc1aa74c05ca5d48fe164b82152b"; - let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); - let res = coin.generate_pod(address.hash).unwrap(); + let address = Address::from_legacyaddress( + "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); + let res = coin.generate_pod(address.hash().clone()).unwrap(); assert_eq!(expected_res, res.to_string()); } @@ -1627,7 +1640,11 @@ fn test_qtum_add_delegation() { keypair.private().secret, )) .unwrap(); - let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); + let address = Address::from_legacyaddress( + "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); let request = QtumDelegationRequest { address: address.to_string(), fee: Some(10), @@ -1666,7 +1683,11 @@ fn test_qtum_add_delegation_on_already_delegating() { keypair.private().secret, )) .unwrap(); - let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); + let address = Address::from_legacyaddress( + "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); let request = QtumDelegationRequest { address: address.to_string(), fee: Some(10), @@ -1873,9 +1894,10 @@ fn test_get_mature_unspent_ordered_map_from_cache_impl( // run test let coin = utxo_coin_for_test(UtxoRpcClientEnum::Electrum(client), None, false); - let (unspents, _) = - block_on(coin.get_mature_unspent_ordered_list(&Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"))) - .expect("Expected an empty unspent list"); + let (unspents, _) = block_on(coin.get_mature_unspent_ordered_list( + &Address::from_legacyaddress("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", &KMD_PREFIXES).unwrap(), + )) + .expect("Expected an empty unspent list"); // unspents should be empty because `is_unspent_mature()` always returns false assert!(unsafe { IS_UNSPENT_MATURE_CALLED }); assert!(unspents.mature.is_empty()); @@ -2014,9 +2036,9 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_tx_in_cache() { let client = native_client_for_test(); let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); - let address: Address = "RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN".into(); + let address: Address = Address::from_legacyaddress("RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN", &KMD_PREFIXES).unwrap(); block_on(coin.as_ref().recently_spent_outpoints.lock()).for_script_pubkey = - Builder::build_p2pkh(&address.hash).to_bytes(); + Builder::build_p2pkh(address.hash()).to_bytes(); // https://morty.explorer.dexstats.info/tx/31c7aaae89ab1c39febae164a3190a86ed7c6c6f8c9dc98ec28d508b7929d347 let tx: UtxoTx = "0400008085202f89027f57730fcbbc2c72fb18bcc3766a713044831a117bb1cade3ed88644864f7333020000006a47304402206e3737b2fcf078b61b16fa67340cc3e79c5d5e2dc9ffda09608371552a3887450220460a332aa1b8ad8f2de92d319666f70751078b221199951f80265b4f7cef8543012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff42b916a80430b80a77e114445b08cf120735447a524de10742fac8f6a9d4170f000000006a473044022004aa053edafb9d161ea8146e0c21ed1593aa6b9404dd44294bcdf920a1695fd902202365eac15dbcc5e9f83e2eed56a8f2f0e5aded36206f9c3fabc668fd4665fa2d012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff03547b16000000000017a9143e8ad0e2bf573d32cb0b3d3a304d9ebcd0c2023b870000000000000000166a144e2b3c0323ab3c2dc6f86dc5ec0729f11e42f56103970400000000001976a91450f4f098306f988d8843004689fae28c83ef16e888ac89c5925f000000000000000000000000000000".into(); @@ -2060,8 +2082,8 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_several_chained_tx let client = native_client_for_test(); let coin = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(client), None, false); - let address: Address = "RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN".into(); - block_on(coin.recently_spent_outpoints.lock()).for_script_pubkey = Builder::build_p2pkh(&address.hash).to_bytes(); + let address: Address = Address::from_legacyaddress("RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN", &KMD_PREFIXES).unwrap(); + block_on(coin.recently_spent_outpoints.lock()).for_script_pubkey = Builder::build_p2pkh(address.hash()).to_bytes(); let coin = utxo_coin_from_fields(coin); // https://morty.explorer.dexstats.info/tx/31c7aaae89ab1c39febae164a3190a86ed7c6c6f8c9dc98ec28d508b7929d347 @@ -2880,7 +2902,9 @@ fn test_tx_details_kmd_rewards() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress( + Address::from_legacyaddress("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk", &KMD_PREFIXES).unwrap(), + ); let coin = utxo_coin_from_fields(fields); let tx_details = get_tx_details_eq_for_both_versions( @@ -2917,7 +2941,9 @@ fn test_tx_details_kmd_rewards_claimed_by_other() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress( + Address::from_legacyaddress("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk", &KMD_PREFIXES).unwrap(), + ); let coin = utxo_coin_from_fields(fields); let tx_details = get_tx_details_eq_for_both_versions(&coin, TX_HASH); @@ -2963,7 +2989,9 @@ fn test_update_kmd_rewards() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress( + Address::from_legacyaddress("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk", &KMD_PREFIXES).unwrap(), + ); let coin = utxo_coin_from_fields(fields); let mut input_transactions = HistoryUtxoTxMap::default(); @@ -2995,7 +3023,9 @@ fn test_update_kmd_rewards_claimed_not_by_me() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress( + Address::from_legacyaddress("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk", &KMD_PREFIXES).unwrap(), + ); let coin = utxo_coin_from_fields(fields); let mut input_transactions = HistoryUtxoTxMap::default(); @@ -3056,14 +3086,16 @@ fn test_withdraw_to_p2pkh() { let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); // Create a p2pkh address for the test coin - let p2pkh_address = Address { - prefix: coin.as_ref().conf.pub_addr_prefix, - hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), - t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Standard, - }; + let p2pkh_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + coin.as_ref().derivation_method.unwrap_single_addr().hash().clone(), + *coin.as_ref().derivation_method.unwrap_single_addr().checksum_type(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_pkh() + .build() + .expect("valid address props"); let withdraw_req = WithdrawRequest { amount: 1.into(), @@ -3078,7 +3110,7 @@ fn test_withdraw_to_p2pkh() { let transaction: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); - let expected_script = Builder::build_p2pkh(&p2pkh_address.hash); + let expected_script = Builder::build_p2pkh(p2pkh_address.hash()); assert_eq!(output_script, expected_script); } @@ -3104,14 +3136,16 @@ fn test_withdraw_to_p2sh() { let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); // Create a p2sh address for the test coin - let p2sh_address = Address { - prefix: coin.as_ref().conf.p2sh_addr_prefix, - hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), - t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Standard, - }; + let p2sh_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + coin.as_ref().derivation_method.unwrap_single_addr().hash().clone(), + *coin.as_ref().derivation_method.unwrap_single_addr().checksum_type(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh() + .build() + .expect("valid address props"); let withdraw_req = WithdrawRequest { amount: 1.into(), @@ -3126,7 +3160,7 @@ fn test_withdraw_to_p2sh() { let transaction: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); - let expected_script = Builder::build_p2sh(&p2sh_address.hash); + let expected_script = Builder::build_p2sh(p2sh_address.hash()); assert_eq!(output_script, expected_script); } @@ -3152,14 +3186,16 @@ fn test_withdraw_to_p2wpkh() { let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, true); // Create a p2wpkh address for the test coin - let p2wpkh_address = Address { - prefix: coin.as_ref().conf.pub_addr_prefix, - hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), - t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Segwit, - }; + let p2wpkh_address = AddressBuilder::new( + UtxoAddressFormat::Segwit, + coin.as_ref().derivation_method.unwrap_single_addr().hash().clone(), + *coin.as_ref().derivation_method.unwrap_single_addr().checksum_type(), + NetworkAddressPrefixes::default(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_pkh() + .build() + .expect("valid address props"); let withdraw_req = WithdrawRequest { amount: 1.into(), @@ -3174,7 +3210,7 @@ fn test_withdraw_to_p2wpkh() { let transaction: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); - let expected_script = Builder::build_p2witness(&p2wpkh_address.hash); + let expected_script = Builder::build_p2wpkh(p2wpkh_address.hash()).expect("valid p2wpkh script"); assert_eq!(output_script, expected_script); } @@ -3205,7 +3241,7 @@ fn test_utxo_standard_with_check_utxo_maturity_true() { let priv_key = Secp256k1Secret::from([1; 32]); let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "RICK", &conf, ¶ms, priv_key)).unwrap(); - let address = Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"); + let address = Address::from_legacyaddress("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", &KMD_PREFIXES).unwrap(); // Don't use `block_on` here because it's used within a mock of [`GetUtxoListOps::get_mature_unspent_ordered_list`]. coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); assert!(unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED }); @@ -3241,7 +3277,7 @@ fn test_utxo_standard_without_check_utxo_maturity() { let priv_key = Secp256k1Secret::from([1; 32]); let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "RICK", &conf, ¶ms, priv_key)).unwrap(); - let address = Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"); + let address = Address::from_legacyaddress("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", &KMD_PREFIXES).unwrap(); // Don't use `block_on` here because it's used within a mock of [`UtxoStandardCoin::get_all_unspent_ordered_list`]. coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); assert!(unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED }); @@ -3276,7 +3312,11 @@ fn test_qtum_without_check_utxo_maturity() { let priv_key = Secp256k1Secret::from([1; 32]); let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); - let address = Address::from("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); + let address = Address::from_legacyaddress( + "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); // Don't use `block_on` here because it's used within a mock of [`QtumCoin::get_mature_unspent_ordered_list`]. coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); assert!(unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED }); @@ -3319,7 +3359,7 @@ fn test_split_qtum() { let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); let p2pkh_address = coin.as_ref().derivation_method.unwrap_single_addr(); - let script: Script = output_script(p2pkh_address, ScriptType::P2PKH); + let script: Script = output_script(p2pkh_address).expect("valid previous script must be built"); let key_pair = coin.as_ref().priv_key_policy.activated_key_or_err().unwrap(); let (unspents, _) = block_on(coin.get_mature_unspent_ordered_list(p2pkh_address)).expect("Unspent list is empty"); log!("Mature unspents vec = {:?}", unspents.mature); @@ -3337,11 +3377,11 @@ fn test_split_qtum() { // fee_amount must be higher than the minimum fee assert!(data.fee_amount > 400_000); log!("Unsigned tx = {:?}", unsigned); - let signature_version = match p2pkh_address.addr_format { + let signature_version = match p2pkh_address.addr_format() { UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, _ => coin.as_ref().conf.signature_version, }; - let prev_script = Builder::build_p2pkh(&p2pkh_address.hash); + let prev_script = output_script(p2pkh_address).expect("valid previous script must be built"); let signed = sign_tx( unsigned, key_pair, @@ -3391,7 +3431,11 @@ fn test_qtum_with_check_utxo_maturity_false() { let priv_key = Secp256k1Secret::from([1; 32]); let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); - let address = Address::from("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); + let address = Address::from_legacyaddress( + "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); // Don't use `block_on` here because it's used within a mock of [`QtumCoin::get_all_unspent_ordered_list`]. coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); assert!(unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED }); @@ -4150,10 +4194,10 @@ fn test_native_display_balances() { let rpc_client = native_client_for_test(); let addresses = vec![ - "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), - "RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), - "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), - "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), + Address::from_legacyaddress("RG278CfeNPFtNztFZQir8cgdWexVhViYVy", &KMD_PREFIXES).unwrap(), + Address::from_legacyaddress("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN", &KMD_PREFIXES).unwrap(), + Address::from_legacyaddress("RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi", &KMD_PREFIXES).unwrap(), + Address::from_legacyaddress("RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF", &KMD_PREFIXES).unwrap(), ]; let actual = rpc_client .display_balances(addresses, TEST_COIN_DECIMALS) @@ -4162,16 +4206,19 @@ fn test_native_display_balances() { let expected: Vec<(Address, BigDecimal)> = vec![ ( - "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), + Address::from_legacyaddress("RG278CfeNPFtNztFZQir8cgdWexVhViYVy", &KMD_PREFIXES).unwrap(), BigDecimal::try_from(5.77699).unwrap(), ), - ("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), BigDecimal::from(0)), ( - "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), + Address::from_legacyaddress("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN", &KMD_PREFIXES).unwrap(), + BigDecimal::from(0), + ), + ( + Address::from_legacyaddress("RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi", &KMD_PREFIXES).unwrap(), BigDecimal::try_from(0.77699).unwrap(), ), ( - "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), + Address::from_legacyaddress("RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF", &KMD_PREFIXES).unwrap(), BigDecimal::try_from(0.99998).unwrap(), ), ]; diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 0ab24bd5fc..795da12006 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -1,7 +1,8 @@ use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::utxo::utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; -use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, FeePolicy, GetUtxoListOps, PrivKeyPolicy, - UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; +use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, AddressBuilder, FeePolicy, + GetUtxoListOps, PrivKeyPolicy, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, + UtxoTx, UTXO_LOCK}; use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionDetails, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; @@ -12,7 +13,7 @@ use crypto::hw_rpc_task::HwRpcTaskAwaitingStatus; use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProcessor}; use crypto::trezor::{TrezorError, TrezorProcessingError}; use crypto::{from_hw_error, CryptoCtx, CryptoCtxError, DerivationPath, HwError, HwProcessingError, HwRpcError}; -use keys::{AddressFormat, AddressHashEnum, KeyPair, Private, Public as PublicKey, Type as ScriptType}; +use keys::{AddressFormat, AddressHashEnum, KeyPair, Private, Public as PublicKey}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc::v1::types::ToTxHash; @@ -87,6 +88,10 @@ impl From for WithdrawError { } } +impl From for WithdrawError { + fn from(e: keys::Error) -> Self { WithdrawError::InternalError(e.to_string()) } +} + #[async_trait] pub trait UtxoWithdraw where @@ -102,16 +107,24 @@ where fn request(&self) -> &WithdrawRequest; fn signature_version(&self) -> SignatureVersion { - match self.sender_address().addr_format { + match self.sender_address().addr_format() { UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, - _ => self.coin().as_ref().conf.signature_version, + UtxoAddressFormat::Standard | UtxoAddressFormat::CashAddress { .. } => { + self.coin().as_ref().conf.signature_version + }, } } - fn prev_script(&self) -> Script { - match self.sender_address().addr_format { - UtxoAddressFormat::Segwit => Builder::build_p2witness(&self.sender_address().hash), - _ => Builder::build_p2pkh(&self.sender_address().hash), + #[allow(clippy::result_large_err)] + fn prev_script(&self) -> Result> { + match self.sender_address().addr_format() { + UtxoAddressFormat::Segwit => match Builder::build_p2wpkh(self.sender_address().hash()) { + Ok(script) => Ok(script), + Err(e) => MmError::err(WithdrawError::InternalError(e.to_string())), + }, + UtxoAddressFormat::Standard | UtxoAddressFormat::CashAddress { .. } => { + Ok(Builder::build_p2pkh(self.sender_address().hash())) + }, } } @@ -127,26 +140,14 @@ where let coin = self.coin(); let ticker = coin.as_ref().conf.ticker.clone(); let decimals = coin.as_ref().decimals; - let conf = &self.coin().as_ref().conf; let req = self.request(); let to = coin.address_from_str(&req.to)?; - let is_p2pkh = to.prefix == conf.pub_addr_prefix && to.t_addr_prefix == conf.pub_t_addr_prefix; - let is_p2sh = to.prefix == conf.p2sh_addr_prefix && to.t_addr_prefix == conf.p2sh_t_addr_prefix; - - let script_type = if is_p2pkh { - ScriptType::P2PKH - } else if is_p2sh { - ScriptType::P2SH - } else { - return MmError::err(WithdrawError::InvalidAddress("Expected either P2PKH or P2SH".into())); - }; - // Generate unsigned transaction. self.on_generating_transaction()?; - let script_pubkey = output_script(&to, script_type).to_bytes(); + let script_pubkey = output_script(&to).map(|script| script.to_bytes())?; let _utxo_lock = UTXO_LOCK.lock().await; let (unspents, _) = coin.get_unspent_ordered_list(&self.sender_address()).await?; @@ -288,7 +289,7 @@ where unsigned_tx .inputs .iter() - .map(|_input| match self.from_address.addr_format { + .map(|_input| match self.from_address.addr_format() { AddressFormat::Segwit => SpendingInputInfo::P2WPKH { address_derivation_path: self.from_derivation_path.clone(), address_pubkey: self.from_pubkey, @@ -310,7 +311,7 @@ where sign_params.add_outputs_infos(once(SendingOutputInfo { destination_address: OutputDestination::change( self.from_derivation_path.clone(), - self.from_address.addr_format.clone(), + self.from_address.addr_format().clone(), ), })); }, @@ -432,7 +433,7 @@ where Ok(with_key_pair::sign_tx( unsigned_tx, &self.key_pair, - self.prev_script(), + self.prev_script()?, self.signature_version(), self.coin.as_ref().conf.fork_id, )?) @@ -464,15 +465,18 @@ where .derivation_method .single_addr_or_err()? .clone() - .addr_format; - let my_address = Address { - prefix: coin.as_ref().conf.pub_addr_prefix, - t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, - hash: AddressHashEnum::AddressHash(key_pair.public().address_hash()), - checksum_type: coin.as_ref().conf.checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), + .addr_format() + .clone(); + let my_address = AddressBuilder::new( addr_format, - }; + AddressHashEnum::AddressHash(key_pair.public().address_hash()), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_pkh() + .build() + .map_to_mm(WithdrawError::InternalError)?; (key_pair, my_address) }, Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { diff --git a/mm2src/coins/utxo_signer/src/with_key_pair.rs b/mm2src/coins/utxo_signer/src/with_key_pair.rs index 5b6a96b993..ada67e2b6d 100644 --- a/mm2src/coins/utxo_signer/src/with_key_pair.rs +++ b/mm2src/coins/utxo_signer/src/with_key_pair.rs @@ -177,7 +177,7 @@ pub fn p2wpkh_spend( let unsigned_input = get_input(signer, input_index)?; let script_code = Builder::build_p2pkh(&key_pair.public().address_hash().into()); // this is the scriptCode by BIP-0143: for P2WPKH scriptCode is P2PKH - let script_pub_key = Builder::build_p2witness(&key_pair.public().address_hash().into()); + let script_pub_key = Builder::build_p2wpkh(&key_pair.public().address_hash().into())?; if script_pub_key != prev_script { return MmError::err(UtxoSignWithKeyPairError::MismatchScript { script_type: "P2WPKH".to_owned(), diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 3d2f8056ea..5d5486cf4c 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1843,7 +1843,7 @@ impl UtxoCommonOps for ZCoin { } fn script_for_address(&self, address: &Address) -> MmResult { - utxo_common::get_script_for_address(self.as_ref(), address) + utxo_common::output_script_checked(self.as_ref(), address) } async fn get_current_mtp(&self) -> UtxoRpcResult { @@ -1921,8 +1921,7 @@ impl UtxoCommonOps for ZCoin { let conf = &self.utxo_arc.conf; utxo_common::address_from_pubkey( pubkey, - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + conf.address_prefixes.clone(), conf.checksum_type, conf.bech32_hrp.clone(), self.addr_format().clone(), diff --git a/mm2src/coins/z_coin/z_coin_errors.rs b/mm2src/coins/z_coin/z_coin_errors.rs index 2a78aedc3f..a708ad1013 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -184,6 +184,7 @@ pub enum SendOutputsErr { Rpc(UtxoRpcError), TxNotMined(String), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + InternalError(String), } impl From for SendOutputsErr { diff --git a/mm2src/coins/z_coin/z_htlc.rs b/mm2src/coins/z_coin/z_htlc.rs index edde4bbc37..6690977bba 100644 --- a/mm2src/coins/z_coin/z_htlc.rs +++ b/mm2src/coins/z_coin/z_htlc.rs @@ -16,8 +16,7 @@ use crate::{PrivKeyPolicyNotAllowed, TransactionEnum}; use bitcrypto::dhash160; use derive_more::Display; use futures::compat::Future01CompatExt; -use keys::Address; -use keys::{KeyPair, Public}; +use keys::{AddressBuilder, KeyPair, Public}; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; use script::Script; @@ -47,14 +46,16 @@ pub async fn z_send_htlc( ) -> Result> { let payment_script = payment_script(time_lock, secret_hash, my_pub, other_pub); let script_hash = dhash160(&payment_script); - let htlc_address = Address { - prefix: coin.utxo_arc.conf.p2sh_addr_prefix, - t_addr_prefix: coin.utxo_arc.conf.p2sh_t_addr_prefix, - hash: script_hash.into(), - checksum_type: coin.utxo_arc.conf.checksum_type, - addr_format: UtxoAddressFormat::Standard, - hrp: None, - }; + let htlc_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + script_hash.into(), + coin.utxo_arc.conf.checksum_type, + coin.utxo_arc.conf.address_prefixes.clone(), + None, + ) + .as_sh() + .build() + .map_to_mm(SendOutputsErr::InternalError)?; let amount_sat = sat_from_big_decimal(&amount, coin.utxo_arc.decimals)?; let address = htlc_address.to_string(); diff --git a/mm2src/mm2_bitcoin/keys/src/address.rs b/mm2src/mm2_bitcoin/keys/src/address.rs index 094f9698b0..ed30b78aef 100644 --- a/mm2src/mm2_bitcoin/keys/src/address.rs +++ b/mm2src/mm2_bitcoin/keys/src/address.rs @@ -5,20 +5,22 @@ //! //! https://en.bitcoin.it/wiki/Address -use base58::{FromBase58, ToBase58}; -use crypto::{checksum, dgroestl512, dhash256, keccak256, ChecksumType}; +use crypto::{dgroestl512, dhash256, keccak256, ChecksumType}; use derive_more::Display; use serde::{Deserialize, Serialize}; use std::fmt; -use std::ops::Deref; use std::str::FromStr; -use {AddressHashEnum, CashAddrType, CashAddress, DisplayLayout, Error, SegwitAddress}; +use {AddressHashEnum, AddressPrefix, CashAddrType, CashAddress, Error, LegacyAddress, NetworkAddressPrefixes, + SegwitAddress}; + +mod address_builder; +pub use self::address_builder::{AddressBuilder, AddressBuilderOption}; /// There are two address formats currently in use. /// https://bitcoin.org/en/developer-reference#address-conversion #[allow(clippy::upper_case_acronyms)] -#[derive(Debug, PartialEq, Clone, Copy)] -pub enum Type { +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum AddressScriptType { /// Pay to PubKey Hash /// Common P2PKH which begin with the number 1, eg: 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2. /// https://bitcoin.org/en/glossary/p2pkh-address @@ -76,24 +78,6 @@ impl AddressFormat { pub fn is_legacy(&self) -> bool { matches!(*self, AddressFormat::Standard) } } -// TODO add ScriptType field to this struct for easier use of output_script function -/// `AddressHash` with prefix and t addr zcash prefix -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub struct Address { - /// The prefix of the address. - pub prefix: u8, - /// T addr prefix, additional prefix used by Zcash and some forks - pub t_addr_prefix: u8, - /// Segwit addr human readable part - pub hrp: Option, - /// Public key hash. - pub hash: AddressHashEnum, - /// Checksum type - pub checksum_type: ChecksumType, - /// Address Format - pub addr_format: AddressFormat, -} - // Todo: add segwit checksum detection pub fn detect_checksum(data: &[u8], checksum: &[u8]) -> Result { if checksum == &dhash256(data)[0..4] { @@ -110,140 +94,94 @@ pub fn detect_checksum(data: &[u8], checksum: &[u8]) -> Result); - -impl Deref for AddressDisplayLayout { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DisplayLayout for Address { - type Target = AddressDisplayLayout; - - fn layout(&self) -> Self::Target { - let mut result = vec![]; - - if self.t_addr_prefix > 0 { - result.push(self.t_addr_prefix); - } - - result.push(self.prefix); - result.extend_from_slice(&self.hash.to_vec()); - let cs = checksum(&result, &self.checksum_type); - result.extend_from_slice(&*cs); - - AddressDisplayLayout(result) - } - - fn from_layout(data: &[u8]) -> Result - where - Self: Sized, - { - match data.len() { - 25 => { - let sum_type = detect_checksum(&data[0..21], &data[21..])?; - - let mut hash = AddressHashEnum::default_address_hash(); - hash.copy_from_slice(&data[1..21]); - - let address = Address { - t_addr_prefix: 0, - prefix: data[0], - hash, - checksum_type: sum_type, - hrp: None, - addr_format: AddressFormat::Standard, - }; - - Ok(address) - }, - 26 => { - let sum_type = detect_checksum(&data[0..22], &data[22..])?; - - let mut hash = AddressHashEnum::default_address_hash(); - hash.copy_from_slice(&data[2..22]); - - let address = Address { - t_addr_prefix: data[0], - prefix: data[1], - hash, - checksum_type: sum_type, - hrp: None, - addr_format: AddressFormat::Standard, - }; - - Ok(address) - }, - _ => Err(Error::InvalidAddress), - } - } +/// Struct for utxo address types representation +/// Contains address hash, format, prefix to get as a string. +/// Also has output ScriptType field to create output script. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct Address { + /// The base58 prefix of the address. + prefix: AddressPrefix, + /// Segwit addr human readable part + hrp: Option, + /// Public key hash. + hash: AddressHashEnum, + /// Checksum type + checksum_type: ChecksumType, + /// Address Format + addr_format: AddressFormat, + // which output script corresponds to this address format and prefix + script_type: AddressScriptType, } -impl fmt::Display for Address { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self.addr_format { - AddressFormat::Segwit => { - SegwitAddress::new(&self.hash, self.hrp.clone().expect("Segwit address should have an hrp")) - .to_string() - .fmt(f) - }, - AddressFormat::CashAddress { - network, - pub_addr_prefix, - p2sh_addr_prefix, - } => { - let cash_address = self - .to_cashaddress(network, *pub_addr_prefix, *p2sh_addr_prefix) - .expect("A valid address"); - cash_address.encode().expect("A valid address").fmt(f) - }, - AddressFormat::Standard => self.layout().to_base58().fmt(f), +impl Address { + pub fn prefix(&self) -> &AddressPrefix { &self.prefix } + pub fn hrp(&self) -> &Option { &self.hrp } + pub fn hash(&self) -> &AddressHashEnum { &self.hash } + pub fn checksum_type(&self) -> &ChecksumType { &self.checksum_type } + pub fn addr_format(&self) -> &AddressFormat { &self.addr_format } + pub fn script_type(&self) -> &AddressScriptType { &self.script_type } + + /// Returns true if output script type is pubkey hash (p2pkh or p2wpkh) + pub fn is_pubkey_hash(&self) -> bool { + if matches!(self.addr_format, AddressFormat::Segwit) { + self.script_type == AddressScriptType::P2WPKH + } else { + self.script_type == AddressScriptType::P2PKH } } -} - -impl FromStr for Address { - type Err = Error; - fn from_str(s: &str) -> Result - where - Self: Sized, - { - let hex = s.from_base58().map_err(|_| Error::InvalidAddress)?; - Address::from_layout(&hex) - } -} - -impl From<&'static str> for Address { - fn from(s: &'static str) -> Self { s.parse().unwrap() } // TODO: dangerous unwrap? -} - -impl Address { pub fn display_address(&self) -> Result { match &self.addr_format { - AddressFormat::Standard => Ok(self.to_string()), + AddressFormat::Standard => { + Ok(LegacyAddress::new(&self.hash, self.prefix.clone(), self.checksum_type).to_string()) + }, AddressFormat::Segwit => match &self.hrp { Some(hrp) => Ok(SegwitAddress::new(&self.hash, hrp.clone()).to_string()), None => Err("Cannot display segwit address for a coin with no bech32_hrp in config".into()), }, - AddressFormat::CashAddress { network, pub_addr_prefix, p2sh_addr_prefix, } => self - .to_cashaddress(network, *pub_addr_prefix, *p2sh_addr_prefix) + .to_cashaddress(network, &NetworkAddressPrefixes { + p2pkh: [*pub_addr_prefix].into(), + p2sh: [*p2sh_addr_prefix].into(), + }) .and_then(|cashaddress| cashaddress.encode()), } } + pub fn from_legacyaddress(s: &str, prefixes: &NetworkAddressPrefixes) -> Result { + let address = LegacyAddress::from_str(s).map_err(|_| String::from("invalid address"))?; + if address.hash.len() != 20 { + return Err("Expect 20 bytes long hash".into()); + } + let mut hash = AddressHashEnum::default_address_hash(); + hash.copy_from_slice(address.hash.as_slice()); + + let script_type = if address.prefix == prefixes.p2pkh { + AddressScriptType::P2PKH + } else if address.prefix == prefixes.p2sh { + AddressScriptType::P2SH + } else { + return Err(String::from("invalid address prefix")); + }; + + Ok(Address { + prefix: address.prefix, + hash, + checksum_type: address.checksum_type, + hrp: None, + addr_format: AddressFormat::Standard, + script_type, + }) + } + pub fn from_cashaddress( cashaddr: &str, checksum_type: ChecksumType, - p2pkh_prefix: u8, - p2sh_prefix: u8, - t_addr_prefix: u8, + net_addr_prefixes: &NetworkAddressPrefixes, ) -> Result { let address = CashAddress::decode(cashaddr)?; @@ -254,57 +192,50 @@ impl Address { let mut hash = AddressHashEnum::default_address_hash(); hash.copy_from_slice(address.hash.as_slice()); - let prefix = match address.address_type { - CashAddrType::P2PKH => p2pkh_prefix, - CashAddrType::P2SH => p2sh_prefix, + let (script_type, addr_prefix) = match address.address_type { + CashAddrType::P2PKH => (AddressScriptType::P2PKH, net_addr_prefixes.p2pkh.clone()), + CashAddrType::P2SH => (AddressScriptType::P2SH, net_addr_prefixes.p2sh.clone()), }; Ok(Address { - prefix, - t_addr_prefix, + prefix: addr_prefix, hash, checksum_type, hrp: None, addr_format: AddressFormat::CashAddress { network: address.prefix.to_string(), - pub_addr_prefix: p2pkh_prefix, - p2sh_addr_prefix: p2sh_prefix, + pub_addr_prefix: net_addr_prefixes.p2pkh.get_size_1_prefix(), + p2sh_addr_prefix: net_addr_prefixes.p2sh.get_size_1_prefix(), }, + script_type, }) } pub fn to_cashaddress( &self, network_prefix: &str, - p2pkh_prefix: u8, - p2sh_prefix: u8, + network_addr_prefixes: &NetworkAddressPrefixes, ) -> Result { - let address_type = if self.prefix == p2pkh_prefix { + let address_type = if self.prefix == network_addr_prefixes.p2pkh { CashAddrType::P2PKH - } else if self.prefix == p2sh_prefix { + } else if self.prefix == network_addr_prefixes.p2sh { CashAddrType::P2SH } else { return Err(format!( "Unknown address prefix {}. Expect: {}, {}", - self.prefix, p2pkh_prefix, p2sh_prefix + self.prefix, network_addr_prefixes.p2pkh, network_addr_prefixes.p2sh )); }; - CashAddress::new(network_prefix, self.hash.to_vec(), address_type) } - pub fn from_segwitaddress( - segaddr: &str, - checksum_type: ChecksumType, - prefix: u8, - t_addr_prefix: u8, - ) -> Result { + pub fn from_segwitaddress(segaddr: &str, checksum_type: ChecksumType) -> Result { let address = SegwitAddress::from_str(segaddr).map_err(|e| e.to_string())?; - let mut hash = if address.program.len() == 20 { - AddressHashEnum::default_address_hash() + let (script_type, mut hash) = if address.program.len() == 20 { + (AddressScriptType::P2WPKH, AddressHashEnum::default_address_hash()) } else if address.program.len() == 32 { - AddressHashEnum::default_witness_script_hash() + (AddressScriptType::P2WSH, AddressHashEnum::default_witness_script_hash()) } else { return Err("Expect either 20 or 32 bytes long hash".into()); }; @@ -313,12 +244,12 @@ impl Address { let hrp = Some(address.hrp); Ok(Address { - prefix, - t_addr_prefix, + prefix: AddressPrefix::default(), hash, checksum_type, hrp, addr_format: AddressFormat::Segwit, + script_type, }) } @@ -330,154 +261,217 @@ impl Address { } } +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.addr_format { + AddressFormat::Segwit => { + SegwitAddress::new(&self.hash, self.hrp.clone().expect("Segwit address should have an hrp")).fmt(f) + }, + AddressFormat::CashAddress { + network, + pub_addr_prefix, + p2sh_addr_prefix, + } => { + let cash_address = self + .to_cashaddress(network, &NetworkAddressPrefixes { + p2pkh: [*pub_addr_prefix].into(), + p2sh: [*p2sh_addr_prefix].into(), + }) + .expect("A valid address"); + cash_address.encode().expect("A valid address").fmt(f) + }, + AddressFormat::Standard => LegacyAddress::new(&self.hash, self.prefix.clone(), self.checksum_type).fmt(f), + } + } +} + #[cfg(test)] mod tests { - use super::{Address, AddressFormat, AddressHashEnum, CashAddrType, CashAddress, ChecksumType}; - use crate::NetworkPrefix; + use super::{Address, AddressBuilder, AddressFormat, AddressHashEnum, CashAddrType, CashAddress, ChecksumType}; + use crate::address_prefixes::prefixes::*; + use crate::{NetworkAddressPrefixes, NetworkPrefix}; #[test] fn test_address_to_string() { - let address = Address { - prefix: 0, - t_addr_prefix: 0, - hash: AddressHashEnum::AddressHash("3f4aa1fedf1f54eeb03b759deadb36676b184911".into()), - checksum_type: ChecksumType::DSHA256, - hrp: None, - addr_format: AddressFormat::Standard, - }; + let address = AddressBuilder::new( + AddressFormat::Standard, + AddressHashEnum::AddressHash("3f4aa1fedf1f54eeb03b759deadb36676b184911".into()), + ChecksumType::DSHA256, + (*BTC_PREFIXES).clone(), + None, + ) + .as_pkh() + .build() + .expect("valid address props"); assert_eq!("16meyfSoQV6twkAAxPe51RtMVz7PGRmWna".to_owned(), address.to_string()); } #[test] fn test_komodo_address_to_string() { - let address = Address { - prefix: 60, - t_addr_prefix: 0, - hash: AddressHashEnum::AddressHash("05aab5342166f8594baf17a7d9bef5d567443327".into()), - checksum_type: ChecksumType::DSHA256, - hrp: None, - addr_format: AddressFormat::Standard, - }; + let address = AddressBuilder::new( + AddressFormat::Standard, + AddressHashEnum::AddressHash("05aab5342166f8594baf17a7d9bef5d567443327".into()), + ChecksumType::DSHA256, + (*KMD_PREFIXES).clone(), + None, + ) + .as_pkh() + .build() + .expect("valid address props"); assert_eq!("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW".to_owned(), address.to_string()); } #[test] fn test_zec_t_address_to_string() { - let address = Address { - t_addr_prefix: 29, - prefix: 37, - hash: AddressHashEnum::AddressHash("05aab5342166f8594baf17a7d9bef5d567443327".into()), - checksum_type: ChecksumType::DSHA256, - hrp: None, - addr_format: AddressFormat::Standard, - }; + let address = AddressBuilder::new( + AddressFormat::Standard, + AddressHashEnum::AddressHash("05aab5342166f8594baf17a7d9bef5d567443327".into()), + ChecksumType::DSHA256, + (*T_ZCASH_PREFIXES).clone(), + None, + ) + .as_pkh() + .build() + .expect("valid address props"); assert_eq!("tmAEKD7psc1ajK76QMGEW8WGQSBBHf9SqCp".to_owned(), address.to_string()); } #[test] fn test_komodo_p2sh_address_to_string() { - let address = Address { - prefix: 85, - t_addr_prefix: 0, - hash: AddressHashEnum::AddressHash("ca0c3786c96ff7dacd40fdb0f7c196528df35f85".into()), - checksum_type: ChecksumType::DSHA256, - hrp: None, - addr_format: AddressFormat::Standard, - }; + let address = AddressBuilder::new( + AddressFormat::Standard, + AddressHashEnum::AddressHash("ca0c3786c96ff7dacd40fdb0f7c196528df35f85".into()), + ChecksumType::DSHA256, + (*KMD_PREFIXES).clone(), + None, + ) + .as_sh() + .build() + .expect("valid address props"); // TODO: check with P2PKH assert_eq!("bX9bppqdGvmCCAujd76Tq76zs1suuPnB9A".to_owned(), address.to_string()); } #[test] fn test_address_from_str() { - let address = Address { - prefix: 0, - t_addr_prefix: 0, - hash: AddressHashEnum::AddressHash("3f4aa1fedf1f54eeb03b759deadb36676b184911".into()), - checksum_type: ChecksumType::DSHA256, - hrp: None, - addr_format: AddressFormat::Standard, - }; + let address = AddressBuilder::new( + AddressFormat::Standard, + AddressHashEnum::AddressHash("3f4aa1fedf1f54eeb03b759deadb36676b184911".into()), + ChecksumType::DSHA256, + (*BTC_PREFIXES).clone(), + None, + ) + .as_pkh() + .build() + .expect("valid address props"); - assert_eq!(address, "16meyfSoQV6twkAAxPe51RtMVz7PGRmWna".into()); + assert_eq!( + address, + Address::from_legacyaddress("16meyfSoQV6twkAAxPe51RtMVz7PGRmWna", &BTC_PREFIXES).unwrap() + ); assert_eq!(address.to_string(), "16meyfSoQV6twkAAxPe51RtMVz7PGRmWna".to_owned()); } #[test] fn test_komodo_address_from_str() { - let address = Address { - prefix: 60, - t_addr_prefix: 0, - hash: AddressHashEnum::AddressHash("05aab5342166f8594baf17a7d9bef5d567443327".into()), - checksum_type: ChecksumType::DSHA256, - hrp: None, - addr_format: AddressFormat::Standard, - }; + let address = AddressBuilder::new( + AddressFormat::Standard, + AddressHashEnum::AddressHash("05aab5342166f8594baf17a7d9bef5d567443327".into()), + ChecksumType::DSHA256, + (*KMD_PREFIXES).clone(), + None, + ) + .as_pkh() + .build() + .expect("valid address props"); - assert_eq!(address, "R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW".into()); + assert_eq!( + address, + Address::from_legacyaddress("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", &KMD_PREFIXES).unwrap() + ); assert_eq!(address.to_string(), "R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW".to_owned()); } #[test] fn test_zec_address_from_str() { - let address = Address { - t_addr_prefix: 29, - prefix: 37, - hash: AddressHashEnum::AddressHash("05aab5342166f8594baf17a7d9bef5d567443327".into()), - checksum_type: ChecksumType::DSHA256, - hrp: None, - addr_format: AddressFormat::Standard, - }; + let address = AddressBuilder::new( + AddressFormat::Standard, + AddressHashEnum::AddressHash("05aab5342166f8594baf17a7d9bef5d567443327".into()), + ChecksumType::DSHA256, + (*T_ZCASH_PREFIXES).clone(), + None, + ) + .as_pkh() + .build() + .expect("valid address props"); - assert_eq!(address, "tmAEKD7psc1ajK76QMGEW8WGQSBBHf9SqCp".into()); + assert_eq!( + address, + Address::from_legacyaddress("tmAEKD7psc1ajK76QMGEW8WGQSBBHf9SqCp", &T_ZCASH_PREFIXES).unwrap() + ); assert_eq!(address.to_string(), "tmAEKD7psc1ajK76QMGEW8WGQSBBHf9SqCp".to_owned()); } #[test] fn test_komodo_p2sh_address_from_str() { - let address = Address { - prefix: 85, - t_addr_prefix: 0, - hash: AddressHashEnum::AddressHash("ca0c3786c96ff7dacd40fdb0f7c196528df35f85".into()), - checksum_type: ChecksumType::DSHA256, - hrp: None, - addr_format: AddressFormat::Standard, - }; + let address = AddressBuilder::new( + AddressFormat::Standard, + AddressHashEnum::AddressHash("ca0c3786c96ff7dacd40fdb0f7c196528df35f85".into()), + ChecksumType::DSHA256, + (*KMD_PREFIXES).clone(), + None, + ) + .as_sh() + .build() + .expect("valid address props"); - assert_eq!(address, "bX9bppqdGvmCCAujd76Tq76zs1suuPnB9A".into()); + assert_eq!( + address, + Address::from_legacyaddress("bX9bppqdGvmCCAujd76Tq76zs1suuPnB9A", &KMD_PREFIXES).unwrap() + ); assert_eq!(address.to_string(), "bX9bppqdGvmCCAujd76Tq76zs1suuPnB9A".to_owned()); } #[test] fn test_grs_addr_from_str() { - let address = Address { - prefix: 36, - t_addr_prefix: 0, - hash: AddressHashEnum::AddressHash("c3f710deb7320b0efa6edb14e3ebeeb9155fa90d".into()), - checksum_type: ChecksumType::DGROESTL512, - hrp: None, - addr_format: AddressFormat::Standard, - }; + let address = AddressBuilder::new( + AddressFormat::Standard, + AddressHashEnum::AddressHash("c3f710deb7320b0efa6edb14e3ebeeb9155fa90d".into()), + ChecksumType::DGROESTL512, + (*GRS_PREFIXES).clone(), + None, + ) + .as_pkh() + .build() + .expect("valid address props"); - assert_eq!(address, "Fo2tBkpzaWQgtjFUkemsYnKyfvd2i8yTki".into()); + assert_eq!( + address, + Address::from_legacyaddress("Fo2tBkpzaWQgtjFUkemsYnKyfvd2i8yTki", &GRS_PREFIXES).unwrap() + ); assert_eq!(address.to_string(), "Fo2tBkpzaWQgtjFUkemsYnKyfvd2i8yTki".to_owned()); } #[test] fn test_smart_addr_from_str() { - let address = Address { - prefix: 63, - t_addr_prefix: 0, - hash: AddressHashEnum::AddressHash("56bb05aa20f5a80cf84e90e5dab05be331333e27".into()), - checksum_type: ChecksumType::KECCAK256, - hrp: None, - addr_format: AddressFormat::Standard, - }; + let address = AddressBuilder::new( + AddressFormat::Standard, + AddressHashEnum::AddressHash("56bb05aa20f5a80cf84e90e5dab05be331333e27".into()), + ChecksumType::KECCAK256, + (*SYS_PREFIXES).clone(), + None, + ) + .as_pkh() + .build() + .expect("valid address props"); - assert_eq!(address, "SVCbBs6FvPYxJrYoJc4TdCe47QNCgmTabv".into()); + assert_eq!( + address, + Address::from_legacyaddress("SVCbBs6FvPYxJrYoJc4TdCe47QNCgmTabv", &SYS_PREFIXES).unwrap() + ); assert_eq!(address.to_string(), "SVCbBs6FvPYxJrYoJc4TdCe47QNCgmTabv".to_owned()); } @@ -495,12 +489,13 @@ mod tests { ]; for i in 0..3 { - let actual_address = Address::from_cashaddress(cashaddresses[i], ChecksumType::DSHA256, 0, 5, 0).unwrap(); - let expected_address: Address = expected[i].into(); + let actual_address = + Address::from_cashaddress(cashaddresses[i], ChecksumType::DSHA256, &BCH_PREFIXES).unwrap(); + let expected_address: Address = Address::from_legacyaddress(expected[i], &BCH_PREFIXES).unwrap(); // comparing only hashes here as Address::from_cashaddress has a different internal format from into() assert_eq!(actual_address.hash, expected_address.hash); let actual_cashaddress = actual_address - .to_cashaddress("bitcoincash", 0, 5) + .to_cashaddress("bitcoincash", &BCH_PREFIXES) .unwrap() .encode() .unwrap(); @@ -515,9 +510,7 @@ mod tests { Address::from_cashaddress( "bitcoincash:qgagf7w02x4wnz3mkwnchut2vxphjzccwxgjvvjmlsxqwkcw59jxxuz", ChecksumType::DSHA256, - 0, - 5, - 0, + &BCH_PREFIXES, ), Err("Expect 20 bytes long hash".into()) ); @@ -525,27 +518,33 @@ mod tests { #[test] fn test_to_cashaddress_err() { - let address = Address { - prefix: 2, - t_addr_prefix: 0, - hash: AddressHashEnum::AddressHash( + let unknown_prefixes: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [2; 1].into(), + p2sh: [2; 1].into(), + }; + let address = AddressBuilder::new( + AddressFormat::CashAddress { + network: "bitcoincash".into(), + pub_addr_prefix: 0, + p2sh_addr_prefix: 5, + }, + AddressHashEnum::AddressHash( [ 140, 0, 44, 191, 189, 83, 144, 173, 47, 216, 127, 59, 80, 232, 159, 100, 156, 132, 78, 192, ] .into(), ), - checksum_type: ChecksumType::DSHA256, - hrp: None, - addr_format: AddressFormat::CashAddress { - network: "bitcoincash".into(), - pub_addr_prefix: 0, - p2sh_addr_prefix: 5, - }, - }; + ChecksumType::DSHA256, + unknown_prefixes, + None, + ) + .as_sh() + .build() + .expect("valid address props"); // actually prefix == 2 is unknown and is neither P2PKH nor P2SH assert_eq!( - address.to_cashaddress("bitcoincash", 0, 5), - Err("Unknown address prefix 2. Expect: 0, 5".into()) + address.to_cashaddress("bitcoincash", &BCH_PREFIXES), + Err("Unknown address prefix [2]. Expect: [0], [5]".into()) ); } @@ -558,7 +557,11 @@ mod tests { ], address_type: CashAddrType::P2PKH, }; - let address: Address = "1DmFp16U73RrVZtYUbo2Ectt8mAnYScpqM".into(); - assert_eq!(address.to_cashaddress("prefix", 0, 5).unwrap(), expected_address); + let address: Address = + Address::from_legacyaddress("1DmFp16U73RrVZtYUbo2Ectt8mAnYScpqM", &BCH_PREFIXES).unwrap(); + assert_eq!( + address.to_cashaddress("prefix", &BCH_PREFIXES).unwrap(), + expected_address + ); } } diff --git a/mm2src/mm2_bitcoin/keys/src/address/address_builder.rs b/mm2src/mm2_bitcoin/keys/src/address/address_builder.rs new file mode 100644 index 0000000000..c292f72510 --- /dev/null +++ b/mm2src/mm2_bitcoin/keys/src/address/address_builder.rs @@ -0,0 +1,141 @@ +use crypto::ChecksumType; +use {Address, AddressFormat, AddressHashEnum, AddressPrefix, AddressScriptType, NetworkAddressPrefixes}; + +/// Params for AddressBuilder to select output script type +#[derive(PartialEq)] +pub enum AddressBuilderOption { + /// build for pay to pubkey hash output (witness or legacy) + BuildAsPubkeyHash, + /// build for pay to script hash output (witness or legacy) + BuildAsScriptHash, +} + +/// Builds Address struct depending on addr_format, validates params to build Address +pub struct AddressBuilder { + /// Coin base58 address prefixes from coin config + prefixes: NetworkAddressPrefixes, + /// Segwit addr human readable part + hrp: Option, + /// Public key hash + hash: AddressHashEnum, + /// Checksum type + checksum_type: ChecksumType, + /// Address Format + addr_format: AddressFormat, + /// Indicate whether tx output for this address is pubkey hash or script hash + build_option: Option, +} + +impl AddressBuilder { + pub fn new( + addr_format: AddressFormat, + hash: AddressHashEnum, + checksum_type: ChecksumType, + prefixes: NetworkAddressPrefixes, + hrp: Option, + ) -> Self { + Self { + addr_format, + hash, + checksum_type, + prefixes, + hrp, + build_option: None, + } + } + + /// Sets build option for Address tx output script type + pub fn with_build_option(mut self, build_option: AddressBuilderOption) -> Self { + self.build_option = Some(build_option); + self + } + + /// Sets Address tx output script type as p2pkh or p2wpkh + pub fn as_pkh(mut self) -> Self { + self.build_option = Some(AddressBuilderOption::BuildAsPubkeyHash); + self + } + + /// Sets Address tx output script type as p2sh or p2wsh + pub fn as_sh(mut self) -> Self { + self.build_option = Some(AddressBuilderOption::BuildAsScriptHash); + self + } + + pub fn build(&self) -> Result { + let build_option = self.build_option.as_ref().ok_or("no address builder option set")?; + match &self.addr_format { + AddressFormat::Standard => Ok(Address { + prefix: self.get_address_prefix(build_option)?, + hrp: None, + hash: self.hash.clone(), + checksum_type: self.checksum_type, + addr_format: self.addr_format.clone(), + script_type: self.get_legacy_script_type(build_option), + }), + AddressFormat::Segwit => { + self.check_segwit_hrp()?; + self.check_segwit_hash(build_option)?; + Ok(Address { + prefix: AddressPrefix::default(), + hrp: self.hrp.clone(), + hash: self.hash.clone(), + checksum_type: self.checksum_type, + addr_format: self.addr_format.clone(), + script_type: self.get_segwit_script_type(build_option), + }) + }, + AddressFormat::CashAddress { .. } => Ok(Address { + prefix: self.get_address_prefix(build_option)?, + hrp: None, + hash: self.hash.clone(), + checksum_type: self.checksum_type, + addr_format: self.addr_format.clone(), + script_type: self.get_legacy_script_type(build_option), + }), + } + } + + fn get_address_prefix(&self, build_option: &AddressBuilderOption) -> Result { + let prefix = match build_option { + AddressBuilderOption::BuildAsPubkeyHash => &self.prefixes.p2pkh, + AddressBuilderOption::BuildAsScriptHash => &self.prefixes.p2sh, + }; + if prefix.is_empty() { + return Err("no prefix for address set".to_owned()); + } + Ok(prefix.clone()) + } + + fn get_legacy_script_type(&self, build_option: &AddressBuilderOption) -> AddressScriptType { + match build_option { + AddressBuilderOption::BuildAsPubkeyHash => AddressScriptType::P2PKH, + AddressBuilderOption::BuildAsScriptHash => AddressScriptType::P2SH, + } + } + + fn get_segwit_script_type(&self, build_option: &AddressBuilderOption) -> AddressScriptType { + match build_option { + AddressBuilderOption::BuildAsPubkeyHash => AddressScriptType::P2WPKH, + AddressBuilderOption::BuildAsScriptHash => AddressScriptType::P2WSH, + } + } + + fn check_segwit_hrp(&self) -> Result<(), String> { + if self.hrp.is_none() { + return Err("no hrp for address".to_owned()); + } + Ok(()) + } + + fn check_segwit_hash(&self, build_option: &AddressBuilderOption) -> Result<(), String> { + let is_hash_valid = match build_option { + AddressBuilderOption::BuildAsPubkeyHash => self.hash.is_address_hash(), + AddressBuilderOption::BuildAsScriptHash => self.hash.is_witness_script_hash(), + }; + if !is_hash_valid { + return Err("invalid hash for segwit address".to_owned()); + } + Ok(()) + } +} diff --git a/mm2src/mm2_bitcoin/keys/src/address_prefixes.rs b/mm2src/mm2_bitcoin/keys/src/address_prefixes.rs new file mode 100644 index 0000000000..1a20e4aa20 --- /dev/null +++ b/mm2src/mm2_bitcoin/keys/src/address_prefixes.rs @@ -0,0 +1,128 @@ +use std::{convert::TryFrom, fmt, u8}; + +/// Prefix for a legacy address (p2pkh or p2sh) +#[derive(Debug, Clone, Eq, Hash, PartialEq, Default)] +pub struct AddressPrefix { + data: Vec, +} + +impl TryFrom<&[u8]> for AddressPrefix { + type Error = (); + + fn try_from(prefix: &[u8]) -> Result { + if !prefix.is_empty() && prefix.len() <= 2 { + Ok(Self { data: prefix.to_vec() }) + } else { + Err(()) + } + } +} + +impl From<[u8; 1]> for AddressPrefix { + fn from(prefix: [u8; 1]) -> Self { Self { data: prefix.to_vec() } } +} + +impl From<[u8; 2]> for AddressPrefix { + fn from(prefix: [u8; 2]) -> Self { Self { data: prefix.to_vec() } } +} + +impl fmt::Display for AddressPrefix { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "[")?; + for i in 0..self.data.len() { + write!(f, "{}", self.data[i])?; + if i < self.data.len() - 1 { + write!(f, ", ")?; + } + } + write!(f, "]")?; + Ok(()) + } +} + +impl AddressPrefix { + /// Get as vec of u8 + pub fn to_vec(&self) -> Vec { self.data.to_vec() } + + /// Get if prefix size is 1, for use in cash_address + pub fn get_size_1_prefix(&self) -> u8 { + if self.data.len() == 1 { + self.data[0] + } else { + 0 // maybe assert should be here as it is not supposed to have other prefix size for cash_address + } + } + + pub fn is_empty(&self) -> bool { self.data.is_empty() } +} + +/// All prefixes for legacy address types supported for a coin, from coin config +#[derive(Debug, Clone, Default)] +pub struct NetworkAddressPrefixes { + pub p2pkh: AddressPrefix, + pub p2sh: AddressPrefix, +} + +impl fmt::Display for NetworkAddressPrefixes { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{{")?; + write!(f, "{}", self.p2pkh)?; + write!(f, "{}", self.p2sh)?; + + write!(f, "}}")?; + Ok(()) + } +} + +/// Some prefixes used in tests +pub mod prefixes { + use super::NetworkAddressPrefixes; + use lazy_static::lazy_static; + + lazy_static! { + pub static ref KMD_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [60].into(), + p2sh: [85].into(), + }; + pub static ref BTC_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [0].into(), + p2sh: [5].into(), + }; + pub static ref T_BTC_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [111].into(), + p2sh: [196].into(), + }; + pub static ref BCH_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [0].into(), + p2sh: [5].into(), + }; + pub static ref QRC20_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [120].into(), + p2sh: [50].into(), + }; + pub static ref QTUM_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [58].into(), + p2sh: [50].into(), + }; + pub static ref T_QTUM_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [120].into(), + p2sh: [110].into(), + }; + pub static ref GRS_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [36].into(), + p2sh: [5].into(), + }; + pub static ref SYS_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [63].into(), + p2sh: [5].into(), + }; + pub static ref ZCASH_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [28, 184].into(), + p2sh: [28, 189].into(), + }; + pub static ref T_ZCASH_PREFIXES: NetworkAddressPrefixes = NetworkAddressPrefixes { + p2pkh: [29, 37].into(), + p2sh: [28, 186].into(), + }; + } +} diff --git a/mm2src/mm2_bitcoin/keys/src/cashaddress.rs b/mm2src/mm2_bitcoin/keys/src/cashaddress.rs index 37066e9a62..b90772dde2 100644 --- a/mm2src/mm2_bitcoin/keys/src/cashaddress.rs +++ b/mm2src/mm2_bitcoin/keys/src/cashaddress.rs @@ -5,7 +5,7 @@ const DEFAULT_PREFIX: NetworkPrefix = NetworkPrefix::BitcoinCash; #[allow(clippy::upper_case_acronyms)] #[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub enum AddressType { +pub enum CashAddrType { /// Pay to PubKey Hash /// https://bitcoin.org/en/glossary/p2pkh-address P2PKH, @@ -77,7 +77,7 @@ impl NetworkPrefix { pub struct CashAddress { pub prefix: NetworkPrefix, pub hash: Vec, - pub address_type: AddressType, + pub address_type: CashAddrType, } impl CashAddress { @@ -159,7 +159,7 @@ impl CashAddress { Ok(format!("{}:{}", self.prefix, address)) } - pub fn new(network_prefix: &str, hash: Vec, address_type: AddressType) -> Result { + pub fn new(network_prefix: &str, hash: Vec, address_type: CashAddrType) -> Result { match hash.len() { 20 | 24 | 28 | 32 | 40 | 48 | 56 | 64 => (), _ => return Err(format!("Unexpected hash size {}", hash.len())), @@ -176,8 +176,8 @@ impl CashAddress { /// Get version byte from fn version_byte(&self) -> Result { let en_address_type: u8 = match self.address_type { - AddressType::P2PKH => 0, - AddressType::P2SH => 1, + CashAddrType::P2PKH => 0, + CashAddrType::P2SH => 1, }; let en_hash_size: u8 = match self.hash.len() { @@ -242,15 +242,15 @@ fn hash_size_from_version(version: u8) -> usize { /// The version byte's most significant bit is reserved and must be 0. /// The 4 next bits indicate the type of address. /// See https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md#version-byte -fn addr_type_from_version(version: u8) -> Result { +fn addr_type_from_version(version: u8) -> Result { if (version & 0b10000000) != 0 { return Err("The version byte's most significant bit is reserved and must be 0".into()); } // shift match version >> 3 { - 0 => Ok(AddressType::P2PKH), - 1 => Ok(AddressType::P2SH), + 0 => Ok(CashAddrType::P2PKH), + 1 => Ok(CashAddrType::P2SH), _ => Err("Unexpected address type".into()), } } @@ -460,28 +460,28 @@ mod tests { hash: vec![ 42, 15, 196, 55, 215, 162, 115, 113, 138, 193, 48, 222, 50, 193, 229, 70, 215, 1, 25, 160, ], - address_type: AddressType::P2SH, + address_type: CashAddrType::P2SH, }, CashAddress { prefix: "bitcoincash".into(), hash: vec![ 195, 247, 16, 222, 183, 50, 11, 14, 250, 110, 219, 20, 227, 235, 238, 185, 21, 95, 169, 13, ], - address_type: AddressType::P2PKH, + address_type: CashAddrType::P2PKH, }, CashAddress { prefix: "bitcoincash".into(), hash: vec![ 195, 247, 16, 222, 183, 50, 11, 14, 250, 110, 219, 20, 227, 235, 238, 185, 21, 95, 169, 13, ], - address_type: AddressType::P2PKH, + address_type: CashAddrType::P2PKH, }, CashAddress { prefix: "bchtest".into(), hash: vec![ 36, 63, 19, 148, 244, 69, 84, 244, 206, 63, 214, 134, 73, 193, 154, 220, 72, 60, 233, 36, ], - address_type: AddressType::P2PKH, + address_type: CashAddrType::P2PKH, }, CashAddress { prefix: "bchtest".into(), @@ -489,7 +489,7 @@ mod tests { 192, 113, 56, 50, 62, 0, 250, 79, 193, 34, 211, 184, 91, 150, 40, 234, 129, 11, 63, 56, 23, 6, 56, 94, 40, 155, 11, 37, 99, 17, 151, 209, 148, 181, 194, 56, 190, 177, 54, 251, ], - address_type: AddressType::P2SH, + address_type: CashAddrType::P2SH, }, ]; diff --git a/mm2src/mm2_bitcoin/keys/src/error.rs b/mm2src/mm2_bitcoin/keys/src/error.rs index fefafc2b83..220bd76b2c 100644 --- a/mm2src/mm2_bitcoin/keys/src/error.rs +++ b/mm2src/mm2_bitcoin/keys/src/error.rs @@ -12,6 +12,7 @@ pub enum Error { InvalidPrivate, InvalidAddress, FailedKeyGeneration, + WitnessHashMismatched, } impl fmt::Display for Error { @@ -26,6 +27,7 @@ impl fmt::Display for Error { Error::InvalidPrivate => "Invalid Private", Error::InvalidAddress => "Invalid Address", Error::FailedKeyGeneration => "Key generation failed", + Error::WitnessHashMismatched => "Witness hash mismatched", }; msg.fmt(f) diff --git a/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs b/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs new file mode 100644 index 0000000000..a9e93127af --- /dev/null +++ b/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs @@ -0,0 +1,106 @@ +use std::str::FromStr; +use std::{convert::TryInto, fmt}; + +use base58::{FromBase58, ToBase58}; +use crypto::{checksum, ChecksumType}; +use std::ops::Deref; +use {AddressHashEnum, AddressPrefix, DisplayLayout}; + +use crate::{address::detect_checksum, Error}; + +/// Struct for legacy address representation. +/// Note: LegacyAddress::from_str deserialization is added, which is used at least in the convertaddress rpc. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Default)] +pub struct LegacyAddress { + /// The prefix of the address. + pub prefix: AddressPrefix, + /// Checksum type + pub checksum_type: ChecksumType, + /// Public key hash. + pub hash: Vec, +} + +pub struct LegacyAddressDisplayLayout(Vec); + +impl Deref for LegacyAddressDisplayLayout { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DisplayLayout for LegacyAddress { + type Target = LegacyAddressDisplayLayout; + + fn layout(&self) -> Self::Target { + let mut result = self.prefix.to_vec(); + result.extend_from_slice(&self.hash.to_vec()); + let cs = checksum(&result, &self.checksum_type); + result.extend_from_slice(&*cs); + + LegacyAddressDisplayLayout(result) + } + + fn from_layout(data: &[u8]) -> Result + where + Self: Sized, + { + match data.len() { + 25 => { + let checksum_type = detect_checksum(&data[0..21], &data[21..])?; + let hash = data[1..21].to_vec(); + + let address = LegacyAddress { + prefix: data[0..1].try_into().expect("prefix conversion should not fail"), + checksum_type, + hash, + }; + + Ok(address) + }, + 26 => { + let checksum_type = detect_checksum(&data[0..22], &data[22..])?; + let hash = data[2..22].to_vec(); + + let address = LegacyAddress { + prefix: data[0..2].try_into().expect("prefix conversion should not fail"), + checksum_type, + hash, + }; + + Ok(address) + }, + _ => Err(Error::InvalidAddress), + } + } +} + +/// Converts legacy addresses from string +impl FromStr for LegacyAddress { + type Err = Error; + + fn from_str(s: &str) -> Result + where + Self: Sized, + { + let hex = s.from_base58().map_err(|_| Error::InvalidAddress)?; + LegacyAddress::from_layout(&hex) + } +} + +impl From<&'static str> for LegacyAddress { + fn from(s: &'static str) -> Self { s.parse().unwrap_or_default() } +} + +impl fmt::Display for LegacyAddress { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { self.layout().to_base58().fmt(fmt) } +} + +impl LegacyAddress { + pub fn new(hash: &AddressHashEnum, prefix: AddressPrefix, checksum_type: ChecksumType) -> LegacyAddress { + LegacyAddress { + prefix, + checksum_type, + hash: hash.to_vec(), + } + } +} diff --git a/mm2src/mm2_bitcoin/keys/src/lib.rs b/mm2src/mm2_bitcoin/keys/src/lib.rs index af93a421ea..c7d28687b9 100644 --- a/mm2src/mm2_bitcoin/keys/src/lib.rs +++ b/mm2src/mm2_bitcoin/keys/src/lib.rs @@ -12,10 +12,12 @@ extern crate serde; #[macro_use] extern crate serde_derive; mod address; +mod address_prefixes; mod cashaddress; mod display; mod error; mod keypair; +mod legacyaddress; mod network; mod private; mod public; @@ -24,11 +26,14 @@ mod signature; pub use primitives::{bytes, hash}; -pub use address::{Address, AddressFormat, Type}; -pub use cashaddress::{AddressType as CashAddrType, CashAddress, NetworkPrefix}; +pub use address::{Address, AddressBuilder, AddressBuilderOption, AddressFormat, AddressScriptType}; +pub use address_prefixes::prefixes; +pub use address_prefixes::{AddressPrefix, NetworkAddressPrefixes}; +pub use cashaddress::{CashAddrType, CashAddress, NetworkPrefix}; pub use display::DisplayLayout; pub use error::Error; pub use keypair::KeyPair; +pub use legacyaddress::LegacyAddress; pub use network::Network; pub use private::Private; pub use public::Public; diff --git a/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs b/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs index eb8112e238..1b3d63b04e 100644 --- a/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs +++ b/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs @@ -58,7 +58,7 @@ impl From for Error { /// The different types of segwit addresses. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum AddressType { +pub enum SegwitAddrType { P2wpkh, /// pay-to-witness-script-hash P2wsh, @@ -86,12 +86,12 @@ impl SegwitAddress { /// Get the address type of the address. /// None if unknown or non-standard. - pub fn address_type(&self) -> Option { + pub fn address_type(&self) -> Option { // BIP-141 p2wpkh or p2wsh addresses. match self.version.to_u8() { 0 => match self.program.len() { - 20 => Some(AddressType::P2wpkh), - 32 => Some(AddressType::P2wsh), + 20 => Some(SegwitAddrType::P2wpkh), + 32 => Some(SegwitAddrType::P2wsh), _ => None, }, _ => None, @@ -211,7 +211,7 @@ mod tests { let hrp = "bc"; let addr = SegwitAddress::new(&AddressHashEnum::AddressHash(hash), hrp.to_string()); assert_eq!(&addr.to_string(), "bc1qvzvkjn4q3nszqxrv3nraga2r822xjty3ykvkuw"); - assert_eq!(addr.address_type(), Some(AddressType::P2wpkh)); + assert_eq!(addr.address_type(), Some(SegwitAddrType::P2wpkh)); } #[test] @@ -225,7 +225,7 @@ mod tests { &addr.to_string(), "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3" ); - assert_eq!(addr.address_type(), Some(AddressType::P2wsh)); + assert_eq!(addr.address_type(), Some(SegwitAddrType::P2wsh)); } #[test] diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/address.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/address.rs index 6147430821..90eeeefc00 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/address.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/address.rs @@ -1,16 +1,19 @@ -use keys::Address; +use keys::LegacyAddress; use serde::de::{Unexpected, Visitor}; use serde::{Deserializer, Serialize, Serializer}; use std::fmt; -pub fn serialize(address: &Address, serializer: S) -> Result +/// Standard serde serialize for LegacyAddress. +pub fn serialize(address: &LegacyAddress, serializer: S) -> Result where S: Serializer, { address.to_string().serialize(serializer) } -pub fn deserialize<'a, D>(deserializer: D) -> Result +/// Standard serde deserialize for LegacyAddress +/// Note: we cannot have the same feature for Address as it must have coin prefixes when deserialized +pub fn deserialize<'a, D>(deserializer: D) -> Result where D: Deserializer<'a>, { @@ -21,7 +24,7 @@ where pub struct AddressVisitor; impl<'b> Visitor<'b> for AddressVisitor { - type Value = Address; + type Value = LegacyAddress; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("an address") } @@ -37,11 +40,11 @@ impl<'b> Visitor<'b> for AddressVisitor { pub mod vec { use super::AddressVisitor; - use keys::Address; + use keys::LegacyAddress; use serde::de::Visitor; use serde::{Deserialize, Deserializer, Serialize, Serializer}; - pub fn serialize(addresses: &[Address], serializer: S) -> Result + pub fn serialize(addresses: &[LegacyAddress], serializer: S) -> Result where S: Serializer, { @@ -52,7 +55,7 @@ pub mod vec { .serialize(serializer) } - pub fn deserialize<'a, D>(deserializer: D) -> Result, D::Error> + pub fn deserialize<'a, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'a>, { @@ -65,24 +68,24 @@ pub mod vec { #[cfg(test)] mod tests { - use keys::Address; + use keys::LegacyAddress; use serde_json; use v1::types; #[derive(Debug, PartialEq, Serialize, Deserialize)] struct TestStruct { #[serde(with = "types::address")] - address: Address, + address: LegacyAddress, } #[derive(Debug, PartialEq, Serialize, Deserialize)] struct VecAddressTest { #[serde(with = "types::address::vec")] - pub addresses: Vec
, + pub addresses: Vec, } impl TestStruct { - fn new(address: Address) -> Self { TestStruct { address } } + fn new(address: LegacyAddress) -> Self { TestStruct { address } } } #[test] diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/mod.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/mod.rs index 5b65f44062..c7b16c672c 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/mod.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/mod.rs @@ -20,8 +20,8 @@ pub use self::hash::{H160, H256, H264}; pub use self::script::ScriptType; pub use self::transaction::{GetRawTransactionResponse, RawTransaction, SignedTransactionInput, SignedTransactionOutput, Transaction, TransactionInput, TransactionInputEnum, - TransactionInputScript, TransactionOutput, TransactionOutputScript, - TransactionOutputWithAddress, TransactionOutputWithScriptData, TransactionOutputs}; + TransactionInputScript, TransactionOutputScript, TransactionOutputWithAddress, + TransactionOutputWithScriptData}; pub use self::uint::U256; pub trait ToTxHash { diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/transaction.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/transaction.rs index 89bc7d3e01..29f04ef99c 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/transaction.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/transaction.rs @@ -2,10 +2,7 @@ use super::bytes::Bytes; use super::hash::H256; use super::script::ScriptType; use keys::Address; -use serde::ser::SerializeMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt; -use v1::types; /// Hex-encoded transaction pub type RawTransaction = Bytes; @@ -37,22 +34,6 @@ pub struct TransactionOutputWithScriptData { pub script_data: Bytes, } -/// Transaction output -#[derive(Debug, PartialEq)] -pub enum TransactionOutput { - /// Of form address: amount - Address(TransactionOutputWithAddress), - /// Of form data: script_data_bytes - ScriptData(TransactionOutputWithScriptData), -} - -/// Transaction outputs, which serializes/deserializes as KV-map -#[derive(Debug, PartialEq)] -pub struct TransactionOutputs { - /// Transaction outputs - pub outputs: Vec, -} - /// Transaction input script #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct TransactionInputScript { @@ -243,78 +224,6 @@ impl Serialize for GetRawTransactionResponse { } } -impl TransactionOutputs { - pub fn len(&self) -> usize { self.outputs.len() } - - pub fn is_empty(&self) -> bool { self.outputs.is_empty() } -} - -impl Serialize for TransactionOutputs { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut state = serializer.serialize_map(Some(self.len()))?; - for output in &self.outputs { - match *output { - TransactionOutput::Address(ref address_output) => { - state.serialize_entry(&address_output.address.to_string(), &address_output.amount)?; - }, - TransactionOutput::ScriptData(ref script_output) => { - state.serialize_entry("data", &script_output.script_data)?; - }, - } - } - state.end() - } -} - -impl<'a> Deserialize<'a> for TransactionOutputs { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'a>, - { - use serde::de::{MapAccess, Visitor}; - - struct TransactionOutputsVisitor; - - impl<'b> Visitor<'b> for TransactionOutputsVisitor { - type Value = TransactionOutputs; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a transaction output object") - } - - fn visit_map(self, mut visitor: V) -> Result - where - V: MapAccess<'b>, - { - let mut outputs: Vec = Vec::with_capacity(visitor.size_hint().unwrap_or(0)); - - while let Some(key) = visitor.next_key::()? { - if &key == "data" { - let value: Bytes = visitor.next_value()?; - outputs.push(TransactionOutput::ScriptData(TransactionOutputWithScriptData { - script_data: value, - })); - } else { - let address = types::address::AddressVisitor::default().visit_str(&key)?; - let amount: f64 = visitor.next_value()?; - outputs.push(TransactionOutput::Address(TransactionOutputWithAddress { - address, - amount, - })); - } - } - - Ok(TransactionOutputs { outputs }) - } - } - - deserializer.deserialize_identifier(TransactionOutputsVisitor) - } -} - #[cfg(test)] mod tests { use super::super::bytes::Bytes; @@ -365,58 +274,6 @@ mod tests { ); } - #[test] - fn transaction_outputs_serialize() { - let txout = TransactionOutputs { - outputs: vec![ - TransactionOutput::Address(TransactionOutputWithAddress { - address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa".into(), - amount: 123.45, - }), - TransactionOutput::Address(TransactionOutputWithAddress { - address: "1H5m1XzvHsjWX3wwU781ubctznEpNACrNC".into(), - amount: 67.89, - }), - TransactionOutput::ScriptData(TransactionOutputWithScriptData { - script_data: Bytes::new(vec![1, 2, 3, 4]), - }), - TransactionOutput::ScriptData(TransactionOutputWithScriptData { - script_data: Bytes::new(vec![5, 6, 7, 8]), - }), - ], - }; - assert_eq!( - serde_json::to_string(&txout).unwrap(), - r#"{"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa":123.45,"1H5m1XzvHsjWX3wwU781ubctznEpNACrNC":67.89,"data":"01020304","data":"05060708"}"# - ); - } - - #[ignore] - #[test] - fn transaction_outputs_deserialize() { - let txout = TransactionOutputs { - outputs: vec![ - TransactionOutput::Address(TransactionOutputWithAddress { - address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa".into(), - amount: 123.45, - }), - TransactionOutput::Address(TransactionOutputWithAddress { - address: "1H5m1XzvHsjWX3wwU781ubctznEpNACrNC".into(), - amount: 67.89, - }), - TransactionOutput::ScriptData(TransactionOutputWithScriptData { - script_data: Bytes::new(vec![1, 2, 3, 4]), - }), - TransactionOutput::ScriptData(TransactionOutputWithScriptData { - script_data: Bytes::new(vec![5, 6, 7, 8]), - }), - ], - }; - assert_eq!( - serde_json::from_str::(r#"{"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa":123.45,"1H5m1XzvHsjWX3wwU781ubctznEpNACrNC":67.89,"data":"01020304","data":"05060708"}"#).unwrap(), - txout); - } - #[test] fn transaction_input_script_serialize() { let txin = TransactionInputScript { diff --git a/mm2src/mm2_bitcoin/script/src/builder.rs b/mm2src/mm2_bitcoin/script/src/builder.rs index 6daccc3136..815e77060e 100644 --- a/mm2src/mm2_bitcoin/script/src/builder.rs +++ b/mm2src/mm2_bitcoin/script/src/builder.rs @@ -1,7 +1,7 @@ //! Script builder use bytes::Bytes; -use keys::{AddressHashEnum, Public}; +use keys::{AddressHashEnum, Error, Public}; use {Num, Opcode, Script}; /// Script builder @@ -39,12 +39,26 @@ impl Builder { .into_script() } - /// Builds p2wpkh or p2wsh script pubkey - pub fn build_p2witness(address: &AddressHashEnum) -> Script { - Builder::default() - .push_opcode(Opcode::OP_0) - .push_bytes(&address.to_vec()) - .into_script() + /// Builds p2wpkh script pubkey + pub fn build_p2wpkh(address_hash: &AddressHashEnum) -> Result { + match address_hash { + AddressHashEnum::AddressHash(wpkh_hash) => Ok(Builder::default() + .push_opcode(Opcode::OP_0) + .push_bytes(wpkh_hash.as_ref()) + .into_script()), + AddressHashEnum::WitnessScriptHash(_) => Err(Error::WitnessHashMismatched), + } + } + + /// Builds p2wsh script pubkey + pub fn build_p2wsh(address_hash: &AddressHashEnum) -> Result { + match address_hash { + AddressHashEnum::WitnessScriptHash(wsh_hash) => Ok(Builder::default() + .push_opcode(Opcode::OP_0) + .push_bytes(wsh_hash.as_ref()) + .into_script()), + AddressHashEnum::AddressHash(_) => Err(Error::WitnessHashMismatched), + } } /// Builds op_return script diff --git a/mm2src/mm2_bitcoin/script/src/script.rs b/mm2src/mm2_bitcoin/script/src/script.rs index a99795b88c..e605dcc8b1 100644 --- a/mm2src/mm2_bitcoin/script/src/script.rs +++ b/mm2src/mm2_bitcoin/script/src/script.rs @@ -33,7 +33,7 @@ pub enum ScriptType { #[derive(PartialEq, Debug)] pub struct ScriptAddress { /// The type of the address. - pub kind: keys::Type, + pub kind: keys::AddressScriptType, /// Public key hash. pub hash: AddressHashEnum, } @@ -42,7 +42,7 @@ impl ScriptAddress { /// Creates P2PKH-type ScriptAddress pub fn new_p2pkh(hash: AddressHashEnum) -> Self { ScriptAddress { - kind: keys::Type::P2PKH, + kind: keys::AddressScriptType::P2PKH, hash, } } @@ -50,7 +50,7 @@ impl ScriptAddress { /// Creates P2SH-type ScriptAddress pub fn new_p2sh(hash: AddressHashEnum) -> Self { ScriptAddress { - kind: keys::Type::P2SH, + kind: keys::AddressScriptType::P2SH, hash, } } @@ -58,7 +58,7 @@ impl ScriptAddress { /// Creates P2WPKH-type ScriptAddress pub fn new_p2wpkh(hash: AddressHashEnum) -> Self { ScriptAddress { - kind: keys::Type::P2WPKH, + kind: keys::AddressScriptType::P2WPKH, hash, } } @@ -66,7 +66,7 @@ impl ScriptAddress { /// Creates P2WSH-type ScriptAddress pub fn new_p2wsh(hash: AddressHashEnum) -> Self { ScriptAddress { - kind: keys::Type::P2WSH, + kind: keys::AddressScriptType::P2WSH, hash, } } @@ -614,7 +614,7 @@ pub fn is_witness_commitment_script(script: &[u8]) -> bool { mod tests { use super::{Script, ScriptAddress, ScriptType}; use crypto::ChecksumType; - use keys::{Address, Public}; + use keys::{prefixes::BTC_PREFIXES, Address, Public}; use {Builder, Error, Opcode}; /// Maximum number of bytes pushable to the stack @@ -790,7 +790,10 @@ OP_ADD #[test] fn test_extract_destinations_pub_key_hash() { - let address = Address::from("13NMTpfNVVJQTNH4spP4UeqBGqLdqDo27S").hash; + let address = Address::from_legacyaddress("13NMTpfNVVJQTNH4spP4UeqBGqLdqDo27S", &BTC_PREFIXES) + .unwrap() + .hash() + .clone(); let script = Builder::build_p2pkh(&address); assert_eq!(script.script_type(), ScriptType::PubKeyHash); assert_eq!( @@ -801,7 +804,10 @@ OP_ADD #[test] fn test_extract_destinations_script_hash() { - let address = Address::from("13NMTpfNVVJQTNH4spP4UeqBGqLdqDo27S").hash; + let address = Address::from_legacyaddress("13NMTpfNVVJQTNH4spP4UeqBGqLdqDo27S", &BTC_PREFIXES) + .unwrap() + .hash() + .clone(); let script = Builder::build_p2sh(&address); assert_eq!(script.script_type(), ScriptType::ScriptHash); assert_eq!( @@ -812,37 +818,33 @@ OP_ADD #[test] fn test_extract_destinations_witness_pub_key_hash() { - let address = Address::from_segwitaddress( - "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", - ChecksumType::DSHA256, - 0, - 0, - ) - .unwrap() - .hash; - let script = Builder::build_p2witness(&address); + let address_hash = + Address::from_segwitaddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", ChecksumType::DSHA256) + .unwrap() + .hash() + .clone(); + let script = Builder::build_p2wpkh(&address_hash).expect("build p2wpkh ok"); assert_eq!(script.script_type(), ScriptType::WitnessKey); assert_eq!( script.extract_destinations(), - Ok(vec![ScriptAddress::new_p2wpkh(address),]) + Ok(vec![ScriptAddress::new_p2wpkh(address_hash),]) ); } #[test] fn test_extract_destinations_witness_script_hash() { - let address = Address::from_segwitaddress( + let address_hash = Address::from_segwitaddress( "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", ChecksumType::DSHA256, - 0, - 0, ) .unwrap() - .hash; - let script = Builder::build_p2witness(&address); + .hash() + .clone(); + let script = Builder::build_p2wsh(&address_hash).expect("build p2wsh ok"); assert_eq!(script.script_type(), ScriptType::WitnessScript); assert_eq!( script.extract_destinations(), - Ok(vec![ScriptAddress::new_p2wsh(address),]) + Ok(vec![ScriptAddress::new_p2wsh(address_hash),]) ); } diff --git a/mm2src/mm2_bitcoin/script/src/sign.rs b/mm2src/mm2_bitcoin/script/src/sign.rs index 1bd01127a4..58b3eea6fa 100644 --- a/mm2src/mm2_bitcoin/script/src/sign.rs +++ b/mm2src/mm2_bitcoin/script/src/sign.rs @@ -627,7 +627,8 @@ mod tests { use bytes::Bytes; use chain::{OutPoint, Transaction, TransactionOutput}; use hash::{H160, H256}; - use keys::{Address, AddressHashEnum, Private}; + use keys::{prefixes::{BTC_PREFIXES, T_BTC_PREFIXES}, + Address, AddressHashEnum, Private}; use script::Script; use ser::deserialize; use sign::SignerHashAlgo; @@ -641,8 +642,8 @@ mod tests { let previous_tx_hash = H256::from_reversed_str("81b4c832d70cb56ff957589752eb4125a4cab78a25a8fc52d6a09e5bd4404d48"); let previous_output_index = 0; - let to: Address = "1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa".into(); - assert!(to.hash.is_address_hash()); + let to: Address = Address::from_legacyaddress("1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa", &BTC_PREFIXES).unwrap(); + assert!(to.hash().is_address_hash()); let previous_output = "76a914df3bd30160e6c6145baaf2c88a8844c13a00d1d588ac".into(); let current_output: Bytes = "76a914c8e90996c7c6080ee06284600c684ed904d14c5c88ac".into(); let value = 91234; @@ -650,8 +651,8 @@ mod tests { // this is irrelevant let mut hash = H160::default(); - if let AddressHashEnum::AddressHash(h) = to.hash { - hash = h; + if let AddressHashEnum::AddressHash(h) = to.hash() { + hash = *h; } assert_eq!(¤t_output[3..23], &*hash); @@ -700,8 +701,8 @@ mod tests { let previous_tx_hash = H256::from_reversed_str("0bc54ed426950f50bf2c2776034a03592e844757b42330eb908eb04492dad2c6"); let previous_output_index = 1; - let to: Address = "msj7SEQmH7pUCUx8YU6R87DrAHYzcABdzw".into(); - assert!(to.hash.is_address_hash()); + let to: Address = Address::from_legacyaddress("msj7SEQmH7pUCUx8YU6R87DrAHYzcABdzw", &T_BTC_PREFIXES).unwrap(); + assert!(to.hash().is_address_hash()); let previous_output = "76a914df3bd30160e6c6145baaf2c88a8844c13a00d1d588ac".into(); let current_output: Bytes = "76a91485ee21a7f8cdd9034fb55004e0d8ed27db1c03c288ac".into(); let value = 100000000; diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index ad8e874819..5012377296 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -26,7 +26,8 @@ use crypto::Secp256k1Secret; use ethereum_types::H160 as H160Eth; use futures01::Future; use http::StatusCode; -use keys::{Address, AddressHashEnum, KeyPair, NetworkPrefix as CashAddrPrefix}; +use keys::{Address, AddressBuilder, AddressHashEnum, AddressPrefix, KeyPair, NetworkAddressPrefixes, + NetworkPrefix as CashAddrPrefix}; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_number::BigDecimal; use mm2_test_helpers::get_passphrase; @@ -251,14 +252,16 @@ impl BchDockerOps { for _ in 0..18 { let key_pair = KeyPair::random_compressed(); let address_hash = key_pair.public().address_hash(); - let address = Address { - prefix: self.coin.as_ref().conf.pub_addr_prefix, - t_addr_prefix: self.coin.as_ref().conf.pub_t_addr_prefix, - hrp: None, - hash: address_hash.into(), - checksum_type: Default::default(), - addr_format: Default::default(), - }; + let address = AddressBuilder::new( + Default::default(), + address_hash.into(), + Default::default(), + self.coin.as_ref().conf.address_prefixes.clone(), + None, + ) + .as_pkh() + .build() + .expect("valid address props"); self.native_client() .import_address(&address.to_string(), &address.to_string(), false) @@ -801,6 +804,8 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { qrc20_coin_conf_item("QORTY"), {"coin":"MYCOIN","asset":"MYCOIN","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, {"coin":"MYCOIN1","asset":"MYCOIN1","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, + // TODO: check if we should fix protocol "type":"UTXO" to "QTUM" for this and other QTUM coin tests. + // Maybe we should use a different coin for "UTXO" protocol and make new tests for "QTUM" protocol {"coin":"QTUM","asset":"QTUM","required_confirmations":0,"decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"segwit":true,"txfee":0,"txfee_volatility_percent":0.1, "mm2":1,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"},"bech32_hrp":"qcrt","address_format":{"format":"segwit"}}, {"coin":"FORSLP","asset":"FORSLP","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"BCH","protocol_data":{"slp_prefix":"slptest"}}}, @@ -990,14 +995,19 @@ pub fn get_balance(mm: &MarketMakerIt, coin: &str) -> BalanceResponse { } pub fn utxo_burn_address() -> Address { - Address { - prefix: 60, - hash: AddressHashEnum::default_address_hash(), - t_addr_prefix: 0, - checksum_type: ChecksumType::DSHA256, - hrp: None, - addr_format: UtxoAddressFormat::Standard, - } + AddressBuilder::new( + UtxoAddressFormat::Standard, + AddressHashEnum::default_address_hash(), + ChecksumType::DSHA256, + NetworkAddressPrefixes { + p2pkh: [60].into(), + p2sh: AddressPrefix::default(), + }, + None, + ) + .as_pkh() + .build() + .expect("valid address props") } pub fn withdraw_max_and_send_v1(mm: &MarketMakerIt, coin: &str, to: &str) -> TransactionDetails { diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 6582f074c3..1b60232955 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -3277,7 +3277,7 @@ fn test_convert_segwit_address() { "!convertaddress success but should be error: {}", rc.1 ); - assert!(rc.1.contains("Expected a valid P2PKH or P2SH prefix for tBTC")); + assert!(rc.1.contains("invalid address prefix")); // test invalid tBTC segwit address let rc = block_on(mm.rpc(&json! ({ @@ -3742,7 +3742,7 @@ fn test_convert_qrc20_address() { rc.1 ); log!("{}", rc.1); - assert!(rc.1.contains("Address has invalid prefixes")); + assert!(rc.1.contains("invalid address prefix")); // test invalid address let rc = block_on(mm.rpc(&json! ({ @@ -3884,7 +3884,7 @@ fn test_validateaddress() { assert!(!result["is_valid"].as_bool().unwrap()); let reason = result["reason"].as_str().unwrap(); log!("{}", reason); - assert!(reason.contains("Expected a valid P2PKH or P2SH prefix")); + assert!(reason.contains("invalid address prefix")); // test invalid ETH address