diff --git a/Cargo.toml b/Cargo.toml index c3af7de..6cd1e07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "electrum2descriptors" -version = "0.5.0" +version = "0.6.0" authors = ["Riccardo Casatta "] edition = "2018" description = "Converts electrum xpubs (like vpub, ypub...) into output descriptors" @@ -23,6 +23,7 @@ path = "src/bin.rs" [dependencies] bitcoin = "0.30" +thiserror = "2" # Optional dependencies serde = { version = "1", optional = true, features = ["derive"] } diff --git a/README.md b/README.md index 7682f72..20c78ce 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Converts [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132. This project consists of a library and an executable. -The work of @ulrichard in this project was sponsored by [SEBA Bank AG](https://seba.swiss) +The work of @ulrichard in this project was sponsored by [AMINA Bank AG](https://aminagroup.com) ## Usage library For the library interface read [the docs](https://docs.rs/electrum2descriptors/latest/libelectrum2descriptors/). diff --git a/src/bin.rs b/src/bin.rs index dd7563a..49797f5 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -1,18 +1,20 @@ #[cfg(feature = "wallet_file")] use libelectrum2descriptors::ElectrumWalletFile; use libelectrum2descriptors::{ - ElectrumExtendedKey, ElectrumExtendedPrivKey, ElectrumExtendedPubKey, + Electrum2DescriptorError, ElectrumExtendedKey, ElectrumExtendedPrivKey, ElectrumExtendedPubKey, }; #[cfg(feature = "wallet_file")] use std::path::Path; use std::str::FromStr; -fn main() -> Result<(), String> { +fn main() -> Result<(), Electrum2DescriptorError> { let mut args = std::env::args(); args.next(); // first is program name let err_msg = "You must specify an extended public or private key or an electrum wallet file as first argument".to_string(); - let electrum_x = args.next().ok_or_else(|| err_msg.clone())?; + let electrum_x = args + .next() + .ok_or_else(|| Electrum2DescriptorError::Generic(err_msg.clone()))?; let descriptor = ElectrumExtendedPrivKey::from_str(&electrum_x) .map(|e| e.to_descriptors()) .or_else(|_| ElectrumExtendedPubKey::from_str(&electrum_x).map(|e| e.to_descriptors())); @@ -20,9 +22,9 @@ fn main() -> Result<(), String> { let descriptor = descriptor.or_else(|_| { let wallet_file = Path::new(&electrum_x) .canonicalize() - .map_err(|_| err_msg.clone())?; + .map_err(|_| Electrum2DescriptorError::Generic(err_msg.clone()))?; if !wallet_file.exists() { - return Err(err_msg); + return Err(Electrum2DescriptorError::Generic(err_msg)); } let wallet = ElectrumWalletFile::from_file(wallet_file.as_path())?; wallet.to_descriptors() diff --git a/src/electrum_extended_priv_key.rs b/src/electrum_extended_priv_key.rs index 8cbda63..d4ecfcf 100644 --- a/src/electrum_extended_priv_key.rs +++ b/src/electrum_extended_priv_key.rs @@ -1,4 +1,4 @@ -use crate::ElectrumExtendedKey; +use crate::{Descriptors, Electrum2DescriptorError, ElectrumExtendedKey}; use bitcoin::base58; use bitcoin::bip32::{ChainCode, ChildNumber, ExtendedPrivKey, Fingerprint}; use bitcoin::secp256k1; @@ -87,19 +87,21 @@ fn initialize_sentinels() -> SentinelMap { } impl FromStr for ElectrumExtendedPrivKey { - type Err = String; + type Err = Electrum2DescriptorError; fn from_str(s: &str) -> Result { - let data = base58::decode_check(s).map_err(|e| e.to_string())?; + let data = base58::decode_check(s)?; if data.len() != 78 { - return Err(base58::Error::InvalidLength(data.len()).to_string()); + return Err(Electrum2DescriptorError::Base58Error( + base58::Error::InvalidLength(data.len()), + )); } let cn_int = u32::from_be_bytes(data[9..13].try_into().unwrap()); let child_number: ChildNumber = ChildNumber::from(cn_int); - let (network, kind) = match_electrum_xprv(&data[0..4]).map_err(|e| e.to_string())?; - let key = secp256k1::SecretKey::from_slice(&data[46..78]).map_err(|e| e.to_string())?; + let (network, kind) = match_electrum_xprv(&data[0..4])?; + let key = secp256k1::SecretKey::from_slice(&data[46..78])?; let xprv = ExtendedPrivKey { network, @@ -125,12 +127,15 @@ impl ElectrumExtendedKey for ElectrumExtendedPrivKey { } /// Returns internal and external descriptor - fn to_descriptors(&self) -> Vec { + fn to_descriptors(&self) -> Descriptors { let xprv = self.xprv.to_string(); let closing_parenthesis = if self.kind.contains('(') { ")" } else { "" }; - (0..=1) - .map(|i| format!("{}({}/{}/*){}", self.kind, xprv, i, closing_parenthesis)) - .collect() + let descriptors = + [0, 1].map(|i| format!("{}({}/{}/*){}", self.kind, xprv, i, closing_parenthesis)); + Descriptors { + external: descriptors[0].clone(), + change: descriptors[1].clone(), + } } } @@ -146,12 +151,12 @@ impl ElectrumExtendedPrivKey { } /// converts to electrum format - pub fn electrum_xprv(&self) -> Result { + pub fn electrum_xprv(&self) -> Result { let sentinels = initialize_sentinels(); let sentinel = sentinels .iter() .find(|sent| sent.1 == self.xprv.network && sent.2 == self.kind) - .ok_or_else(|| "unknown type".to_string())?; + .ok_or_else(|| Electrum2DescriptorError::UnknownType)?; let mut data = Vec::from(&sentinel.0[..]); data.push(self.xprv.depth); data.extend(self.xprv.parent_fingerprint.as_bytes()); @@ -162,7 +167,9 @@ impl ElectrumExtendedPrivKey { data.extend(self.xprv.private_key.as_ref()); if data.len() != 78 { - return Err(base58::Error::InvalidLength(data.len()).to_string()); + return Err(Electrum2DescriptorError::Base58Error( + base58::Error::InvalidLength(data.len()), + )); } Ok(base58::encode_check(&data)) @@ -191,8 +198,8 @@ mod tests { assert_eq!(electrum_xprv.xprv.to_string(),"xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD"); assert_eq!(electrum_xprv.kind, "sh(wpkh"); let descriptors = electrum_xprv.to_descriptors(); - assert_eq!(descriptors[0], "sh(wpkh(xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD/0/*))"); - assert_eq!(descriptors[1], "sh(wpkh(xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD/1/*))"); + assert_eq!(descriptors.external, "sh(wpkh(xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD/0/*))"); + assert_eq!(descriptors.change, "sh(wpkh(xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD/1/*))"); let xprv = electrum_xprv.xprv(); assert_eq!(xprv.to_string(), "xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD"); } diff --git a/src/electrum_extended_pub_key.rs b/src/electrum_extended_pub_key.rs index 77d08e3..3b2f8e2 100644 --- a/src/electrum_extended_pub_key.rs +++ b/src/electrum_extended_pub_key.rs @@ -1,4 +1,4 @@ -use crate::ElectrumExtendedKey; +use crate::{Descriptors, Electrum2DescriptorError, ElectrumExtendedKey}; use bitcoin::base58; use bitcoin::bip32::{ChainCode, ChildNumber, ExtendedPubKey, Fingerprint}; use bitcoin::secp256k1; @@ -87,18 +87,20 @@ fn initialize_sentinels() -> SentinelMap { } impl FromStr for ElectrumExtendedPubKey { - type Err = String; + type Err = Electrum2DescriptorError; fn from_str(s: &str) -> Result { - let data = base58::decode_check(s).map_err(|e| e.to_string())?; + let data = base58::decode_check(s)?; if data.len() != 78 { - return Err(base58::Error::InvalidLength(data.len()).to_string()); + return Err(Electrum2DescriptorError::Base58Error( + base58::Error::InvalidLength(data.len()), + )); } let cn_int = u32::from_be_bytes(data[9..13].try_into().unwrap()); let child_number: ChildNumber = ChildNumber::from(cn_int); - let (network, kind) = match_electrum_xpub(&data[0..4]).map_err(|e| e.to_string())?; + let (network, kind) = match_electrum_xpub(&data[0..4])?; let xpub = ExtendedPubKey { network, @@ -106,8 +108,7 @@ impl FromStr for ElectrumExtendedPubKey { parent_fingerprint: Fingerprint::from(&data[5..9].try_into().unwrap()), child_number, chain_code: ChainCode::from(&data[13..45].try_into().unwrap()), - public_key: secp256k1::PublicKey::from_slice(&data[45..78]) - .map_err(|e| e.to_string())?, + public_key: secp256k1::PublicKey::from_slice(&data[45..78])?, }; Ok(ElectrumExtendedPubKey { xpub, kind }) } @@ -125,12 +126,15 @@ impl ElectrumExtendedKey for ElectrumExtendedPubKey { } /// Returns internal and external descriptor - fn to_descriptors(&self) -> Vec { + fn to_descriptors(&self) -> Descriptors { let xpub = self.xpub.to_string(); let closing_parenthesis = if self.kind.contains('(') { ")" } else { "" }; - (0..=1) - .map(|i| format!("{}({}/{}/*){}", self.kind, xpub, i, closing_parenthesis)) - .collect() + let descriptors = + [0, 1].map(|i| format!("{}({}/{}/*){}", self.kind, xpub, i, closing_parenthesis)); + Descriptors { + external: descriptors[0].clone(), + change: descriptors[1].clone(), + } } } @@ -146,12 +150,12 @@ impl ElectrumExtendedPubKey { } /// converts to electrum format - pub fn electrum_xpub(&self) -> Result { + pub fn electrum_xpub(&self) -> Result { let sentinels = initialize_sentinels(); let sentinel = sentinels .iter() .find(|sent| sent.1 == self.xpub.network && sent.2 == self.kind) - .ok_or_else(|| "unknown type".to_string())?; + .ok_or_else(|| Electrum2DescriptorError::UnknownType)?; let mut data = Vec::from(&sentinel.0[..]); data.push(self.xpub.depth); data.extend(self.xpub.parent_fingerprint.as_bytes()); @@ -161,7 +165,9 @@ impl ElectrumExtendedPubKey { data.extend(&self.xpub.public_key.serialize()); // or serialize_uncompressed if data.len() != 78 { - return Err(base58::Error::InvalidLength(data.len()).to_string()); + return Err(Electrum2DescriptorError::Base58Error( + base58::Error::InvalidLength(data.len()), + )); } Ok(base58::encode_check(&data)) @@ -192,8 +198,8 @@ mod tests { assert_eq!(electrum_xpub.xpub.to_string(),"tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp"); assert_eq!(electrum_xpub.kind, "wpkh"); let descriptors = electrum_xpub.to_descriptors(); - assert_eq!(descriptors[0], "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/0/*)"); - assert_eq!(descriptors[1], "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/1/*)"); + assert_eq!(descriptors.external, "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/0/*)"); + assert_eq!(descriptors.change, "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/1/*)"); let xpub = electrum_xpub.xpub(); assert_eq!(xpub.to_string(), "tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp"); } @@ -230,7 +236,7 @@ mod tests { assert_eq!(electrum_xpub.xpub.network, Network::Bitcoin); let descriptors = electrum_xpub.to_descriptors(); let descriptor: miniscript::Descriptor = - descriptors[0].parse().unwrap(); + descriptors.external.parse().unwrap(); let secp = Secp256k1::verification_only(); let first_address = descriptor .at_derivation_index(0) diff --git a/src/electrum_wallet_file.rs b/src/electrum_wallet_file.rs index 2b23d60..d53b642 100644 --- a/src/electrum_wallet_file.rs +++ b/src/electrum_wallet_file.rs @@ -1,4 +1,7 @@ -use crate::{ElectrumExtendedKey, ElectrumExtendedPrivKey, ElectrumExtendedPubKey}; +use crate::{ + Descriptors, Electrum2DescriptorError, ElectrumExtendedKey, ElectrumExtendedPrivKey, + ElectrumExtendedPubKey, +}; use bitcoin::bip32::{ExtendedPrivKey, ExtendedPubKey}; use regex::Regex; use serde::{de, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; @@ -14,7 +17,10 @@ pub struct ElectrumWalletFile { impl ElectrumWalletFile { /// Construct a wallet - pub fn new(keystores: &[Keystore], min_signatures: u8) -> Result { + pub fn new( + keystores: &[Keystore], + min_signatures: u8, + ) -> Result { let wallet = if keystores.len() == 1 { ElectrumWalletFile { addresses: Addresses::new(), @@ -22,10 +28,7 @@ impl ElectrumWalletFile { keystores: keystores.to_vec(), } } else if keystores.len() >= 255 { - return Err(format!( - "keystore sizes aboce 255 are not currently supported. {}", - keystores.len() - )); + return Err(Electrum2DescriptorError::TooManyKeyStores(keystores.len())); } else { ElectrumWalletFile { addresses: Addresses::new(), @@ -53,21 +56,26 @@ impl ElectrumWalletFile { } /// Parse an electrum wallet file - pub fn from_file(wallet_file: &Path) -> Result { - let file = std::fs::File::open(wallet_file).map_err(|e| e.to_string())?; + pub fn from_file(wallet_file: &Path) -> Result { + let file = std::fs::File::open(wallet_file)?; let reader = BufReader::new(file); - let wallet = serde_json::from_reader(reader).map_err(|e| e.to_string())?; + let wallet = serde_json::from_reader(reader)?; Ok(wallet) } /// Write to an electrum wallet file - pub fn to_file(&self, wallet_file: &Path) -> Result<(), String> { - let file = std::fs::File::create(wallet_file).map_err(|e| e.to_string())?; - serde_json::to_writer_pretty(file, self).map_err(|e| e.to_string()) + pub fn to_file(&self, wallet_file: &Path) -> Result<(), Electrum2DescriptorError> { + let file = std::fs::File::create(wallet_file)?; + Ok(serde_json::to_writer_pretty(file, self)?) + } + + /// Write to a string as electrum wallet file format + pub fn to_string(&self) -> Result { + Ok(serde_json::to_string_pretty(self)?) } /// Construct from an output descriptor. Only the external descriptor is needed, the change descriptor is implied. - pub fn from_descriptor(desc: &str) -> Result { + pub fn from_descriptor(desc: &str) -> Result { let wallet = if desc.contains("(sortedmulti(") { ElectrumWalletFile::from_descriptor_multisig(desc) } else { @@ -78,10 +86,9 @@ impl ElectrumWalletFile { } /// Construct from a single signature output descriptor. Only the external descriptor is needed, the change descriptor is implied. - fn from_descriptor_singlesig(desc: &str) -> Result { + fn from_descriptor_singlesig(desc: &str) -> Result { let re = - Regex::new(r#"(pkh|sh\(wpkh|sh\(wsh|wpkh|wsh)\((([tx]p(ub|rv)[0-9A-Za-z]+)/0/\*)\)+"#) - .map_err(|e| e.to_string())?; + Regex::new(r#"(pkh|sh\(wpkh|sh\(wsh|wpkh|wsh)\((([tx]p(ub|rv)[0-9A-Za-z]+)/0/\*)\)+"#)?; let captures = re.captures(desc).map(|captures| { captures .iter() @@ -93,7 +100,12 @@ impl ElectrumWalletFile { }); let keystore = match captures.as_deref() { Some([kind, _, xkey]) => Keystore::new(kind, xkey)?, - _ => return Err(format!("Unknown descriptor format: {:?}", captures)), + _ => { + return Err(Electrum2DescriptorError::UnknownDescriptorFormat(format!( + "{:?}", + captures + ))) + } }; Ok(ElectrumWalletFile { @@ -104,11 +116,10 @@ impl ElectrumWalletFile { } /// Construct from a multisig output descriptor. Only the external descriptor is needed, the change descriptor is implied. - fn from_descriptor_multisig(desc: &str) -> Result { + fn from_descriptor_multisig(desc: &str) -> Result { let re = Regex::new( r#"(sh|sh\(wsh|wsh)\(sortedmulti\((\d),([tx]p(ub|rv)[0-9A-Za-z]+/0/\*,?)+\)+"#, - ) - .map_err(|e| e.to_string())?; + )?; let captures = re.captures(desc).map(|captures| { captures .iter() @@ -123,18 +134,20 @@ impl ElectrumWalletFile { "wsh" => "wsh", "sh" => "pkh", "sh(wsh" => "sh(wsh", - _ => return Err(format!("unknown nultisig kind: {}", kind)), + _ => { + return Err(Electrum2DescriptorError::UnknownScriptKind( + kind.to_string(), + )) + } }; - let re = Regex::new(r#"[tx]p[ur][bv][0-9A-Za-z]+"#).map_err(|e| e.to_string())?; + let re = Regex::new(r#"[tx]p[ur][bv][0-9A-Za-z]+"#)?; let keystores = re .captures_iter(desc) .map(|cap| Keystore::new(kind, &cap[0])) .collect::, _>>()?; let y = keystores.len(); if y < 2 { - return Err( - "Multisig with less than two signers doesn't make a lot of sense".to_string(), - ); + return Err(Electrum2DescriptorError::MultisigFewSigners); } Ok(ElectrumWalletFile { @@ -143,21 +156,25 @@ impl ElectrumWalletFile { wallet_type: WalletType::Multisig(x.parse().unwrap(), y as u8), }) } else { - Err(format!( - "Unknown multisig descriptor format: {:?}", + Err(Electrum2DescriptorError::UnknownDescriptorFormat(format!( + "{:?}", captures - )) + ))) } } /// Generate output descriptors matching the electrum wallet - pub fn to_descriptors(&self) -> Result, String> { + pub fn to_descriptors(&self) -> Result { match self.wallet_type { WalletType::Standard => { let exkey = self.keystores[0].get_xkey()?; let desc_ext = exkey.kind().to_string() + "(" + &exkey.xkey_str() + "/0/*)"; let desc_chg = exkey.kind().to_string() + "(" + &exkey.xkey_str() + "/1/*)"; - Ok(vec![desc_ext, desc_chg]) + + Ok(Descriptors { + external: desc_ext, + change: desc_chg, + }) } WalletType::Multisig(x, _y) => { let xkeys = self @@ -183,31 +200,33 @@ impl ElectrumWalletFile { }; let desc_chg = desc.replace("/0/*", "/1/*"); - Ok(vec![desc, desc_chg]) + Ok(Descriptors { + external: desc, + change: desc_chg, + }) } } } /// validate the internal structure - fn validate(&self) -> Result<(), String> { + fn validate(&self) -> Result<(), Electrum2DescriptorError> { let expected_keystores: usize = match self.wallet_type { WalletType::Standard => 1, WalletType::Multisig(_x, y) => y.into(), }; if self.keystores.len() != expected_keystores { - return Err(format!( - "Wrong number of keystores: {}; expected: {}", + return Err(Electrum2DescriptorError::WrongNumberOfKeyStores( self.keystores.len(), - expected_keystores + expected_keystores, )); } if let WalletType::Multisig(x, _y) = self.wallet_type { if x as usize > expected_keystores { - return Err(format!( - "Minimum number of signatures {} must not be greater than keystores {}", - x, expected_keystores + return Err(Electrum2DescriptorError::NumberSignaturesKeyStores( + x, + expected_keystores, )); } } @@ -216,6 +235,15 @@ impl ElectrumWalletFile { } } +impl FromStr for ElectrumWalletFile { + type Err = Electrum2DescriptorError; + + /// Parse an electrum wallet file from string + fn from_str(wallet_file: &str) -> Result { + Ok(serde_json::from_str(wallet_file)?) + } +} + impl Serialize for ElectrumWalletFile { fn serialize(&self, serializer: S) -> Result where @@ -384,7 +412,7 @@ pub struct Keystore { impl Keystore { /// Construct a Keystore from script kind and xpub or xprv - fn new(kind: &str, xkey: &str) -> Result { + fn new(kind: &str, xkey: &str) -> Result { let xprv = ExtendedPrivKey::from_str(xkey); let exprv = if let Ok(xprv) = xprv { Some(ElectrumExtendedPrivKey::new(xprv, kind.to_string()).electrum_xprv()?) @@ -396,10 +424,7 @@ impl Keystore { let secp = bitcoin::secp256k1::Secp256k1::new(); ElectrumExtendedPubKey::new(ExtendedPubKey::from_priv(&secp, &xprv), kind.to_string()) } else { - ElectrumExtendedPubKey::new( - ExtendedPubKey::from_str(xkey).map_err(|e| e.to_string())?, - kind.to_string(), - ) + ElectrumExtendedPubKey::new(ExtendedPubKey::from_str(xkey)?, kind.to_string()) } .electrum_xpub()?; @@ -411,7 +436,7 @@ impl Keystore { } /// Get the xprv if available or else the xpub. - fn get_xkey(&self) -> Result, String> { + fn get_xkey(&self) -> Result, Electrum2DescriptorError> { if let Some(xprv) = &self.xprv { let exprv = ElectrumExtendedPrivKey::from_str(xprv)?; return Ok(Box::new(exprv)); @@ -441,11 +466,11 @@ impl fmt::Display for WalletType { } impl FromStr for WalletType { - type Err = String; + type Err = Electrum2DescriptorError; /// Parse WalletType from a string representation fn from_str(wallet_type: &str) -> Result { - let re = Regex::new(r#"(standard)|(\d+)(of)(\d+)"#).map_err(|e| e.to_string())?; + let re = Regex::new(r#"(standard)|(\d+)(of)(\d+)"#)?; let captures = re.captures(wallet_type).map(|captures| { captures .iter() @@ -457,7 +482,9 @@ impl FromStr for WalletType { match captures.as_deref() { Some(["standard"]) => Ok(WalletType::Standard), Some([x, "of", y]) => Ok(WalletType::Multisig(x.parse().unwrap(), y.parse().unwrap())), - _ => Err(format!("Unknown wallet type: {}", wallet_type)), + _ => Err(Electrum2DescriptorError::UnknownWalletType( + wallet_type.to_string(), + )), } } } diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..6636a47 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,41 @@ +use bitcoin::{base58, bip32, secp256k1}; +use serde_json::Error as SerdeError; +use std::io; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Electrum2DescriptorError { + #[error(transparent)] + Serde(#[from] SerdeError), + #[error(transparent)] + IO(#[from] io::Error), + #[error(transparent)] + Infallible(#[from] std::convert::Infallible), + #[error(transparent)] + Base58Error(#[from] base58::Error), + #[error(transparent)] + Secp256k1Error(#[from] secp256k1::Error), + #[error(transparent)] + Bip32Error(#[from] bip32::Error), + #[error(transparent)] + RegexError(#[from] regex::Error), + + #[error("Unknown type")] + UnknownType, + #[error("Unknown wallet type: {0}")] + UnknownWalletType(String), + #[error("Multisig with less than two signers doesn't make a lot of sense")] + MultisigFewSigners, + #[error("Unknown multisig descriptor format: {0}")] + UnknownDescriptorFormat(String), + #[error("Wrong number of keystores: {0}; expected: {1}")] + WrongNumberOfKeyStores(usize, usize), + #[error("Minimum number of signatures {0} must not be greater than keystores {1}")] + NumberSignaturesKeyStores(u8, usize), + #[error("keystore sizes above 255 are not currently supported. {0}")] + TooManyKeyStores(usize), + #[error("Unknown script kind: {0}")] + UnknownScriptKind(String), + #[error("{0}")] + Generic(String), +} diff --git a/src/lib.rs b/src/lib.rs index 8fa4c4d..f7dc75c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,15 +2,17 @@ pub mod electrum_extended_priv_key; pub mod electrum_extended_pub_key; #[cfg(feature = "wallet_file")] pub mod electrum_wallet_file; +pub mod errors; pub use electrum_extended_priv_key::ElectrumExtendedPrivKey; pub use electrum_extended_pub_key::ElectrumExtendedPubKey; #[cfg(feature = "wallet_file")] pub use electrum_wallet_file::ElectrumWalletFile; +pub use errors::Electrum2DescriptorError; pub trait ElectrumExtendedKey { /// Returns internal and external descriptor - fn to_descriptors(&self) -> Vec; + fn to_descriptors(&self) -> Descriptors; /// Returns the bitcoin extended key (xpub or xprv) as String fn xkey_str(&self) -> String; @@ -18,3 +20,10 @@ pub trait ElectrumExtendedKey { /// Returns the kind of script fn kind(&self) -> &str; } + +/// The two descriptors for external and change addresses +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Descriptors { + pub external: String, + pub change: String, +} diff --git a/tests/wallets2descriptors.rs b/tests/wallets2descriptors.rs index 5b3b44b..94b11f9 100644 --- a/tests/wallets2descriptors.rs +++ b/tests/wallets2descriptors.rs @@ -2,7 +2,10 @@ use bdk::{bitcoin::Network, database::MemoryDatabase, wallet::AddressIndex, Wallet}; use libelectrum2descriptors::ElectrumWalletFile; use rstest::rstest; -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; use tempfile::tempdir; #[rstest] @@ -42,7 +45,8 @@ fn parse_wallet( fn wallet_name_to_descriptors(wallet_name: &str) -> Vec { let wallet_file = get_test_wallet_file(wallet_name); let wallet = ElectrumWalletFile::from_file(wallet_file.as_path()).unwrap(); - wallet.to_descriptors().unwrap() + let descriptors = wallet.to_descriptors().unwrap(); + vec![descriptors.external, descriptors.change] } fn get_test_wallet_file(wallet_name: &str) -> PathBuf { @@ -105,5 +109,32 @@ fn descriptor_electrum_wallet_roundtrip(#[case] wallet_name: &str, #[case] descr assert_eq!(wallet, imported); let desc = wallet.to_descriptors().unwrap(); - assert_eq!(desc[0], descriptor); + assert_eq!(desc.external, descriptor); +} + +#[rstest] +#[case::default_legacy( + "pkh(tprv8ZgxMBicQKsPeYnCHtn5QZqhTgkkDmXebfQMXWmX7ThXJFCbzDTKFNRsB43GUmHzu2pdGcnnegFy175kFcgZQYC5BFPnRdYDPQyqetpyjb5/0/*)")] +#[case::default_legacy_watch( + "pkh(tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt/0/*)")] +#[case::default_segwit( + "wpkh(tprv8cvkZzx9zA7EfFDbH945mK23r7hg6EHXUk79wVUSRukwyctFS1AdpSpkZcykAMDveCj8RA3R4jwFTKMwMbWexJox8NMqq7YphJLDumfCSfu/0/*)")] +#[case::multisig_hw_segwit( + "wsh(sortedmulti(2,tpubDEcw4ooTbmw62zBKdkYepoP3z4WWugdeRzPHHAbk8XVsPfBE9AAZMNghiqwtdFgtabaeppBTPmezUkRkQZidLcSJp3XTASbMakHcYauWehZ/0/*,tpubDEbkvhmJoZMq3SUNqEf3aEsubvqsCUPc7rroHkGERgS7qA1gQVMxUPrgzth6x43odirLohwf4aMHpvcnWi3jCB2xkizv8T4B2KqLRZVLC6K/0/*))")] +#[case::multisig_legacy( + "sh(sortedmulti(2,tprv8ZgxMBicQKsPeLPWr5WbJDAhANr6irc1Yf7eUNCYjGYap27HU4bDBXWGMT3X75FhDyxNXr6pK4QeHcCBvkqchQzK8wZ4JbGv5X5MWtXQtqy/0/*,tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt/0/*))")] +#[case::multisig_segwit( + "wsh(sortedmulti(2,tprv8dNybiDsdyms39SAWTxyiNHABTTgiqmJpScmxGrdKEuZ7TwXcaYXT4f4ddVjWiiQs9zowHqyDmvaebN6fU2Lu6iAYnYuepiLkvzGdcZZi8D/0/*,tpubD9cniQzQ8XnuagyP9Xwg3sWCX77wQPWoLPW7jqzcPn37r8hq2X86uztCEyFbMY16amzwdJ1CcNRXhF3vykn1wuDv2ULzryRtaCcN5Cr8F9y/0/*))")] +#[case::multisig_wrapped_watch( + "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))")] +fn descriptor_string_roundtrip(#[case] descriptor: &str) { + let wallet = ElectrumWalletFile::from_descriptor(descriptor).unwrap(); + + let electrum_string = wallet.to_string().unwrap(); + + let imported = ElectrumWalletFile::from_str(&electrum_string).unwrap(); + assert_eq!(wallet, imported); + + let desc = wallet.to_descriptors().unwrap(); + assert_eq!(desc.external, descriptor); }