├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── bin.rs ├── electrum_extended_priv_key.rs ├── electrum_extended_pub_key.rs ├── electrum_wallet_file.rs ├── errors.rs └── lib.rs └── tests ├── wallets ├── default_legacy ├── default_legacy_watch ├── default_segwit ├── imported_addr ├── imported_privkey ├── multisig_hw_segwit ├── multisig_legacy ├── multisig_segwit └── multisig_wrapped_watch └── wallets2descriptors.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: {} 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build-test: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | rust: 20 | - 1.82.0 21 | - 1.70.0 # MSRV 22 | features: 23 | - "" 24 | - default 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: dtolnay/rust-toolchain@master 29 | with: 30 | toolchain: ${{ matrix.rust }} 31 | components: rustfmt, clippy 32 | - name: Build 33 | run: cargo build --no-default-features --features "${{ matrix.features }}" 34 | - name: Run tests 35 | run: cargo test --no-default-features --features "${{ matrix.features }}" 36 | - name: Clippy 37 | run: cargo clippy -- -D warnings 38 | 39 | fmt: 40 | name: Rust fmt 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v2 45 | - name: Setup Rust Toolchain 46 | uses: dtolnay/rust-toolchain@master 47 | with: 48 | toolchain: stable 49 | components: rustfmt, clippy 50 | - name: Check fmt 51 | run: cargo fmt --all -- --check 52 | 53 | audit: 54 | runs-on: ubuntu-latest 55 | permissions: 56 | issues: write 57 | steps: 58 | - uses: actions/checkout@v3 59 | - uses: actions-rust-lang/audit@v1 60 | name: Audit Rust Dependencies 61 | # with: 62 | # Comma separated list of issues to ignore 63 | # ignore: RUSTSEC-2020-0036 64 | 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea/ 3 | *.iml 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "electrum2descriptors" 3 | version = "0.7.0" 4 | authors = ["Riccardo Casatta "] 5 | edition = "2018" 6 | description = "Converts electrum xpubs (like vpub, ypub...) into output descriptors" 7 | license = "MIT" 8 | homepage = "https://github.com/RCasatta/electrum2descriptors" 9 | documentation = "https://docs.rs/electrum2descriptors/" 10 | repository = "https://github.com/RCasatta/electrum2descriptors" 11 | keywords = ["bitcoin", "electrum", "descriptors"] 12 | readme = "README.md" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [lib] 17 | name = "libelectrum2descriptors" 18 | path = "src/lib.rs" 19 | 20 | [[bin]] 21 | name = "electrum2descriptors" 22 | path = "src/bin.rs" 23 | 24 | [dependencies] 25 | bitcoin = "0.32" 26 | thiserror = "2" 27 | 28 | # Optional dependencies 29 | serde = { version = "1", optional = true, features = ["derive"] } 30 | serde_json = { version = "1", optional = true } 31 | regex = { version = "1", optional = true } 32 | 33 | [dev-dependencies] 34 | miniscript = "12" 35 | bdk_wallet = "1.0.0" 36 | rstest = "0.17" 37 | tempfile = "3.5" 38 | 39 | [features] 40 | default = [ "wallet_file" ] 41 | wallet_file = [ "serde", "serde_json", "regex"] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Riccardo Casatta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electrum2descriptors 2 | 3 | [![crates.io](https://img.shields.io/crates/v/electrum2descriptors.svg)](https://crates.io/crates/electrum2descriptors) 4 | [![rustc](https://img.shields.io/badge/rustc-1.61%2B-lightgrey.svg)](https://blog.rust-lang.org/2022/05/19/Rust-1.61.0.html) 5 | 6 | Converts [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) extended keys (like the vpub, ypub, yprv, etc. used by Electrum) into [output descriptors](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md) 7 | 8 | This project consists of a library and an executable. 9 | 10 | The work of @ulrichard in this project was sponsored by [AMINA Bank AG](https://aminagroup.com) 11 | 12 | ## Usage library 13 | For the library interface read [the docs](https://docs.rs/electrum2descriptors/latest/libelectrum2descriptors/). 14 | With the library, you can also convert from descriptor to slip-0132 and to electrum wallet files. 15 | 16 | ## Usage binary 17 | 18 | ``` 19 | $ cargo install electrum2descriptors 20 | $ electrum2descriptors vpub5VXaSncXqxLbdmvrC4Y8z9CszPwuEscADoetWhfrxDFzPUbL5nbVtanYDkrVEutkv9n5A5aCcvRC9swbjDKgHjCZ2tAeae8VsBuPbS8KpXv 21 | ["wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/0/*)", "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/1/*)"] 22 | ``` 23 | 24 | or 25 | 26 | ``` 27 | git clone https://github.com/RCasatta/electrum2descriptors 28 | cd electrum2descriptors 29 | cargo run -- vpub5VXaSncXqxLbdmvrC4Y8z9CszPwuEscADoetWhfrxDFzPUbL5nbVtanYDkrVEutkv9n5A5aCcvRC9swbjDKgHjCZ2tAeae8VsBuPbS8KpXv 30 | ["wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/0/*)", "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/1/*)"] 31 | ``` 32 | 33 | can also convert electrum wallet files to descriptors 34 | 35 | ``` 36 | $ cargo run -- tests/wallets/default_segwit 37 | ["wpkh(tprv8cvkZzx9zA7EfFDbH945mK23r7hg6EHXUk79wVUSRukwyctFS1AdpSpkZcykAMDveCj8RA3R4jwFTKMwMbWexJox8NMqq7YphJLDumfCSfu/0/*)", "wpkh(tprv8cvkZzx9zA7EfFDbH945mK23r7hg6EHXUk79wVUSRukwyctFS1AdpSpkZcykAMDveCj8RA3R4jwFTKMwMbWexJox8NMqq7YphJLDumfCSfu/1/*)"] 38 | ``` 39 | -------------------------------------------------------------------------------- /src/bin.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "wallet_file")] 2 | use libelectrum2descriptors::ElectrumWalletFile; 3 | use libelectrum2descriptors::{ 4 | Electrum2DescriptorError, ElectrumExtendedKey, ElectrumExtendedPrivKey, ElectrumExtendedPubKey, 5 | }; 6 | #[cfg(feature = "wallet_file")] 7 | use std::path::Path; 8 | use std::str::FromStr; 9 | 10 | fn main() -> Result<(), Electrum2DescriptorError> { 11 | let mut args = std::env::args(); 12 | args.next(); // first is program name 13 | let err_msg = 14 | "You must specify an extended public or private key or an electrum wallet file as first argument"; 15 | let electrum_x = args 16 | .next() 17 | .ok_or_else(|| Electrum2DescriptorError::GenericBorrow(err_msg))?; 18 | let descriptor = ElectrumExtendedPrivKey::from_str(&electrum_x) 19 | .map(|e| e.to_descriptors()) 20 | .or_else(|_| ElectrumExtendedPubKey::from_str(&electrum_x).map(|e| e.to_descriptors())); 21 | #[cfg(feature = "wallet_file")] 22 | let descriptor = descriptor.or_else(|_| { 23 | let wallet_file = Path::new(&electrum_x) 24 | .canonicalize() 25 | .map_err(|_| Electrum2DescriptorError::GenericBorrow(err_msg))?; 26 | if !wallet_file.exists() { 27 | return Err(Electrum2DescriptorError::GenericBorrow(err_msg)); 28 | } 29 | let wallet = ElectrumWalletFile::from_file(wallet_file.as_path())?; 30 | wallet.to_descriptors() 31 | }); 32 | 33 | println!("{:?}", descriptor?); 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /src/electrum_extended_priv_key.rs: -------------------------------------------------------------------------------- 1 | use crate::{Descriptors, Electrum2DescriptorError, ElectrumExtendedKey}; 2 | use bitcoin::base58; 3 | use bitcoin::bip32::{ChainCode, ChildNumber, Fingerprint, Xpriv}; 4 | use bitcoin::secp256k1; 5 | use bitcoin::{Network, NetworkKind}; 6 | use std::convert::TryInto; 7 | use std::str::FromStr; 8 | 9 | pub struct ElectrumExtendedPrivKey { 10 | xprv: Xpriv, 11 | kind: String, 12 | } 13 | 14 | type SentinelMap = Vec<([u8; 4], Network, String)>; 15 | fn initialize_sentinels() -> SentinelMap { 16 | // electrum testnet 17 | // https://github.com/spesmilo/electrum/blob/928e43fc530ba5befa062db788e4e04d56324161/electrum/constants.py#L110-L116 18 | // XPRV_HEADERS = { 19 | // 'standard': 0x04358394, # tprv 20 | // 'p2wpkh-p2sh': 0x044a4e28, # uprv 21 | // 'p2wsh-p2sh': 0x024285b5, # Uprv 22 | // 'p2wpkh': 0x045f18bc, # vprv 23 | // 'p2wsh': 0x02575048, # Vprv 24 | // } 25 | // electrum mainnet 26 | // https://github.com/spesmilo/electrum/blob/928e43fc530ba5befa062db788e4e04d56324161/electrum/constants.py#L74-L80 27 | // XPRV_HEADERS = { 28 | // 'standard': 0x0488ade4, # xprv 29 | // 'p2wpkh-p2sh': 0x049d7878, # yprv 30 | // 'p2wsh-p2sh': 0x0295b005, # Yprv 31 | // 'p2wpkh': 0x04b2430c, # zprv 32 | // 'p2wsh': 0x02aa7a99, # Zprv 33 | // } 34 | 35 | vec![ 36 | ( 37 | [0x04u8, 0x35, 0x83, 0x94], 38 | Network::Testnet, 39 | "pkh".to_string(), 40 | ), // tprv 41 | ( 42 | [0x04u8, 0x4a, 0x4e, 0x28], 43 | Network::Testnet, 44 | "sh(wpkh".to_string(), 45 | ), // uprv 46 | ( 47 | [0x02u8, 0x42, 0x85, 0xb5], 48 | Network::Testnet, 49 | "sh(wsh".to_string(), 50 | ), // Uprv 51 | ( 52 | [0x04u8, 0x5f, 0x18, 0xbc], 53 | Network::Testnet, 54 | "wpkh".to_string(), 55 | ), // vprv 56 | ( 57 | [0x02u8, 0x57, 0x50, 0x48], 58 | Network::Testnet, 59 | "wsh".to_string(), 60 | ), // Vprv 61 | ( 62 | [0x04u8, 0x88, 0xad, 0xE4], 63 | Network::Bitcoin, 64 | "pkh".to_string(), 65 | ), // xprv 66 | ( 67 | [0x04u8, 0x9d, 0x78, 0x78], 68 | Network::Bitcoin, 69 | "sh(wpkh".to_string(), 70 | ), // yprv 71 | ( 72 | [0x02u8, 0x95, 0xb0, 0x05], 73 | Network::Bitcoin, 74 | "sh(wsh".to_string(), 75 | ), // Yprv 76 | ( 77 | [0x04u8, 0xb2, 0x43, 0x0c], 78 | Network::Bitcoin, 79 | "wpkh".to_string(), 80 | ), // zprv 81 | ( 82 | [0x02u8, 0xaa, 0x7a, 0x99], 83 | Network::Bitcoin, 84 | "wsh".to_string(), 85 | ), // Zprv 86 | ] 87 | } 88 | 89 | impl FromStr for ElectrumExtendedPrivKey { 90 | type Err = Electrum2DescriptorError; 91 | 92 | fn from_str(s: &str) -> Result { 93 | let data = base58::decode_check(s)?; 94 | 95 | if data.len() != 78 { 96 | return Err(Electrum2DescriptorError::InvalidLength(data.len())); 97 | } 98 | 99 | let cn_int = u32::from_be_bytes(data[9..13].try_into().unwrap()); 100 | let child_number: ChildNumber = ChildNumber::from(cn_int); 101 | let (network, kind) = match_electrum_xprv(&data[0..4])?; 102 | let key = secp256k1::SecretKey::from_slice(&data[46..78])?; 103 | 104 | let xprv = Xpriv { 105 | network: network.into(), 106 | depth: data[4], 107 | parent_fingerprint: Fingerprint::from(&data[5..9].try_into().unwrap()), 108 | child_number, 109 | chain_code: ChainCode::from(&data[13..45].try_into().unwrap()), 110 | private_key: key, 111 | }; 112 | Ok(ElectrumExtendedPrivKey { xprv, kind }) 113 | } 114 | } 115 | 116 | impl ElectrumExtendedKey for ElectrumExtendedPrivKey { 117 | /// Returns the kind 118 | fn kind(&self) -> &str { 119 | &self.kind 120 | } 121 | 122 | /// Returns the xprv as String 123 | fn xkey_str(&self) -> String { 124 | self.xprv.to_string() 125 | } 126 | 127 | /// Returns internal and external descriptor 128 | fn to_descriptors(&self) -> Descriptors { 129 | let xprv = self.xprv.to_string(); 130 | let closing_parenthesis = if self.kind.contains('(') { ")" } else { "" }; 131 | let [external, change] = 132 | [0, 1].map(|i| format!("{}({}/{}/*){}", self.kind, xprv, i, closing_parenthesis)); 133 | Descriptors { external, change } 134 | } 135 | } 136 | 137 | impl ElectrumExtendedPrivKey { 138 | /// Constructs a new instance 139 | pub fn new(xprv: Xpriv, kind: String) -> Self { 140 | ElectrumExtendedPrivKey { xprv, kind } 141 | } 142 | 143 | /// Returns the xprv 144 | pub fn xprv(&self) -> &Xpriv { 145 | &self.xprv 146 | } 147 | 148 | /// converts to electrum format 149 | pub fn electrum_xprv(&self) -> Result { 150 | let sentinels = initialize_sentinels(); 151 | let sentinel = sentinels 152 | .iter() 153 | .find(|sent| NetworkKind::from(sent.1) == self.xprv.network && sent.2 == self.kind) 154 | .ok_or_else(|| Electrum2DescriptorError::UnknownType)?; 155 | let mut data = Vec::from(&sentinel.0[..]); 156 | data.push(self.xprv.depth); 157 | data.extend(self.xprv.parent_fingerprint.as_bytes()); 158 | let child_number: u32 = self.xprv.child_number.into(); 159 | data.extend(child_number.to_be_bytes()); 160 | data.extend(self.xprv.chain_code.as_bytes()); 161 | data.push(0u8); 162 | data.extend(self.xprv.private_key.as_ref()); 163 | 164 | if data.len() != 78 { 165 | return Err(Electrum2DescriptorError::InvalidLength(data.len())); 166 | } 167 | 168 | Ok(base58::encode_check(&data)) 169 | } 170 | } 171 | 172 | fn match_electrum_xprv(version: &[u8]) -> Result<(Network, String), Electrum2DescriptorError> { 173 | let sentinels = initialize_sentinels(); 174 | let sentinel = sentinels 175 | .iter() 176 | .find(|sent| sent.0 == version) 177 | .ok_or_else(|| { 178 | Electrum2DescriptorError::InvalidExtendedKeyVersion(version[0..4].try_into().unwrap()) 179 | })?; 180 | Ok((sentinel.1, sentinel.2.clone())) 181 | } 182 | 183 | #[cfg(test)] 184 | mod tests { 185 | use super::*; 186 | use std::str::FromStr; 187 | 188 | #[test] 189 | fn test_vprv_from_electrum() { 190 | let electrum_xprv = ElectrumExtendedPrivKey::from_str("yprvAHwhK6RbpuS3dgCYHM5jc2ZvEKd7Bi61u9FVhYMpgMSuZS613T1xxQeKTffhrHY79hZ5PsskBjcc6C2V7DrnsMsNaGDaWev3GLRQRgV7hxF").unwrap(); 191 | assert_eq!(electrum_xprv.xprv.to_string(),"xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD"); 192 | assert_eq!(electrum_xprv.kind, "sh(wpkh"); 193 | let descriptors = electrum_xprv.to_descriptors(); 194 | assert_eq!(descriptors.external, "sh(wpkh(xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD/0/*))"); 195 | assert_eq!(descriptors.change, "sh(wpkh(xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD/1/*))"); 196 | let xprv = electrum_xprv.xprv(); 197 | assert_eq!(xprv.to_string(), "xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD"); 198 | } 199 | 200 | #[test] 201 | fn test_vprv_to_electrum() { 202 | let electrum_xprv = ElectrumExtendedPrivKey::new( 203 | Xpriv::from_str("xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD").unwrap(), 204 | "sh(wpkh".to_string(), 205 | ); 206 | assert_eq!(electrum_xprv.electrum_xprv().unwrap(), "yprvAHwhK6RbpuS3dgCYHM5jc2ZvEKd7Bi61u9FVhYMpgMSuZS613T1xxQeKTffhrHY79hZ5PsskBjcc6C2V7DrnsMsNaGDaWev3GLRQRgV7hxF"); 207 | } 208 | 209 | #[test] 210 | fn test_vprv_roundtrip() { 211 | let elxprv = "yprvAHwhK6RbpuS3dgCYHM5jc2ZvEKd7Bi61u9FVhYMpgMSuZS613T1xxQeKTffhrHY79hZ5PsskBjcc6C2V7DrnsMsNaGDaWev3GLRQRgV7hxF"; 212 | let electrum_xprv = ElectrumExtendedPrivKey::from_str(elxprv).unwrap(); 213 | assert_eq!(electrum_xprv.electrum_xprv().unwrap(), elxprv); 214 | assert_ne!(electrum_xprv.xprv.to_string(), elxprv); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/electrum_extended_pub_key.rs: -------------------------------------------------------------------------------- 1 | use crate::{Descriptors, Electrum2DescriptorError, ElectrumExtendedKey}; 2 | use bitcoin::base58; 3 | use bitcoin::bip32::{ChainCode, ChildNumber, Fingerprint, Xpub}; 4 | use bitcoin::secp256k1; 5 | use bitcoin::{Network, NetworkKind}; 6 | use std::convert::TryInto; 7 | use std::str::FromStr; 8 | 9 | pub struct ElectrumExtendedPubKey { 10 | xpub: Xpub, 11 | kind: String, 12 | } 13 | 14 | type SentinelMap = Vec<([u8; 4], Network, String)>; 15 | fn initialize_sentinels() -> SentinelMap { 16 | // electrum testnet 17 | // https://github.com/spesmilo/electrum/blob/928e43fc530ba5befa062db788e4e04d56324161/electrum/constants.py#L118-L124 18 | // XPUB_HEADERS = { 19 | // 'standard': 0x043587cf, # tpub 20 | // 'p2wpkh-p2sh': 0x044a5262, # upub 21 | // 'p2wsh-p2sh': 0x024289ef, # Upub 22 | // 'p2wpkh': 0x045f1cf6, # vpub 23 | // 'p2wsh': 0x02575483, # Vpub 24 | // } 25 | // electrum mainnet 26 | // https://github.com/spesmilo/electrum/blob/928e43fc530ba5befa062db788e4e04d56324161/electrum/constants.py#L82-L88 27 | // XPUB_HEADERS = { 28 | // 'standard': 0x0488b21e, # xpub 29 | // 'p2wpkh-p2sh': 0x049d7cb2, # ypub 30 | // 'p2wsh-p2sh': 0x0295b43f, # Ypub 31 | // 'p2wpkh': 0x04b24746, # zpub 32 | // 'p2wsh': 0x02aa7ed3, # Zpub 33 | // } 34 | 35 | vec![ 36 | ( 37 | [0x04u8, 0x35, 0x87, 0xcf], 38 | Network::Testnet, 39 | "pkh".to_string(), 40 | ), // tpub 41 | ( 42 | [0x04u8, 0x4a, 0x52, 0x62], 43 | Network::Testnet, 44 | "sh(wpkh".to_string(), 45 | ), // upub 46 | ( 47 | [0x02u8, 0x42, 0x89, 0xef], 48 | Network::Testnet, 49 | "sh(wsh".to_string(), 50 | ), // Upub 51 | ( 52 | [0x04u8, 0x5f, 0x1c, 0xf6], 53 | Network::Testnet, 54 | "wpkh".to_string(), 55 | ), // vpub 56 | ( 57 | [0x02u8, 0x57, 0x54, 0x83], 58 | Network::Testnet, 59 | "wsh".to_string(), 60 | ), // Vpub 61 | ( 62 | [0x04u8, 0x88, 0xB2, 0x1E], 63 | Network::Bitcoin, 64 | "pkh".to_string(), 65 | ), // xpub 66 | ( 67 | [0x04u8, 0x9d, 0x7c, 0xb2], 68 | Network::Bitcoin, 69 | "sh(wpkh".to_string(), 70 | ), // ypub 71 | ( 72 | [0x02u8, 0x95, 0xb4, 0x3f], 73 | Network::Bitcoin, 74 | "sh(wsh".to_string(), 75 | ), // Ypub 76 | ( 77 | [0x04u8, 0xb2, 0x47, 0x46], 78 | Network::Bitcoin, 79 | "wpkh".to_string(), 80 | ), // zpub 81 | ( 82 | [0x02u8, 0xaa, 0x7e, 0xd3], 83 | Network::Bitcoin, 84 | "wsh".to_string(), 85 | ), // Zpub 86 | ] 87 | } 88 | 89 | impl FromStr for ElectrumExtendedPubKey { 90 | type Err = Electrum2DescriptorError; 91 | 92 | fn from_str(s: &str) -> Result { 93 | let data = base58::decode_check(s)?; 94 | 95 | if data.len() != 78 { 96 | return Err(Electrum2DescriptorError::InvalidLength(data.len())); 97 | } 98 | 99 | let cn_int = u32::from_be_bytes(data[9..13].try_into().unwrap()); 100 | let child_number: ChildNumber = ChildNumber::from(cn_int); 101 | let (network, kind) = match_electrum_xpub(&data[0..4])?; 102 | 103 | let xpub = Xpub { 104 | network: network.into(), 105 | depth: data[4], 106 | parent_fingerprint: Fingerprint::from(&data[5..9].try_into().unwrap()), 107 | child_number, 108 | chain_code: ChainCode::from(&data[13..45].try_into().unwrap()), 109 | public_key: secp256k1::PublicKey::from_slice(&data[45..78])?, 110 | }; 111 | Ok(ElectrumExtendedPubKey { xpub, kind }) 112 | } 113 | } 114 | 115 | impl ElectrumExtendedKey for ElectrumExtendedPubKey { 116 | /// Returns the kind 117 | fn kind(&self) -> &str { 118 | &self.kind 119 | } 120 | 121 | /// Returns the xpub as String 122 | fn xkey_str(&self) -> String { 123 | self.xpub.to_string() 124 | } 125 | 126 | /// Returns internal and external descriptor 127 | fn to_descriptors(&self) -> Descriptors { 128 | let xpub = self.xpub.to_string(); 129 | let closing_parenthesis = if self.kind.contains('(') { ")" } else { "" }; 130 | let [external, change] = 131 | [0, 1].map(|i| format!("{}({}/{}/*){}", self.kind, xpub, i, closing_parenthesis)); 132 | Descriptors { external, change } 133 | } 134 | } 135 | 136 | impl ElectrumExtendedPubKey { 137 | /// Constructs a new instance 138 | pub fn new(xpub: Xpub, kind: String) -> Self { 139 | ElectrumExtendedPubKey { xpub, kind } 140 | } 141 | 142 | /// Returns the xpub 143 | pub fn xpub(&self) -> &Xpub { 144 | &self.xpub 145 | } 146 | 147 | /// converts to electrum format 148 | pub fn electrum_xpub(&self) -> Result { 149 | let sentinels = initialize_sentinels(); 150 | let sentinel = sentinels 151 | .iter() 152 | .find(|sent| NetworkKind::from(sent.1) == self.xpub.network && sent.2 == self.kind) 153 | .ok_or_else(|| Electrum2DescriptorError::UnknownType)?; 154 | let mut data = Vec::from(&sentinel.0[..]); 155 | data.push(self.xpub.depth); 156 | data.extend(self.xpub.parent_fingerprint.as_bytes()); 157 | let child_number: u32 = self.xpub.child_number.into(); 158 | data.extend(child_number.to_be_bytes()); 159 | data.extend(self.xpub.chain_code.as_bytes()); 160 | data.extend(&self.xpub.public_key.serialize()); // or serialize_uncompressed 161 | 162 | if data.len() != 78 { 163 | return Err(Electrum2DescriptorError::InvalidLength(data.len())); 164 | } 165 | 166 | Ok(base58::encode_check(&data)) 167 | } 168 | } 169 | 170 | fn match_electrum_xpub(version: &[u8]) -> Result<(Network, String), Electrum2DescriptorError> { 171 | let sentinels = initialize_sentinels(); 172 | let sentinel = sentinels 173 | .iter() 174 | .find(|sent| sent.0 == version) 175 | .ok_or_else(|| { 176 | Electrum2DescriptorError::InvalidExtendedKeyVersion(version[0..4].try_into().unwrap()) 177 | })?; 178 | Ok((sentinel.1, sentinel.2.clone())) 179 | } 180 | 181 | #[cfg(test)] 182 | mod tests { 183 | use super::*; 184 | use miniscript::bitcoin::secp256k1::Secp256k1; 185 | use miniscript::descriptor::DescriptorPublicKey; 186 | use std::str::FromStr; 187 | 188 | #[test] 189 | fn test_vpub_from_electrum() { 190 | let electrum_xpub = ElectrumExtendedPubKey::from_str("vpub5VXaSncXqxLbdmvrC4Y8z9CszPwuEscADoetWhfrxDFzPUbL5nbVtanYDkrVEutkv9n5A5aCcvRC9swbjDKgHjCZ2tAeae8VsBuPbS8KpXv").unwrap(); 191 | assert_eq!(electrum_xpub.xpub.to_string(),"tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp"); 192 | assert_eq!(electrum_xpub.kind, "wpkh"); 193 | let descriptors = electrum_xpub.to_descriptors(); 194 | assert_eq!(descriptors.external, "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/0/*)"); 195 | assert_eq!(descriptors.change, "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/1/*)"); 196 | let xpub = electrum_xpub.xpub(); 197 | assert_eq!(xpub.to_string(), "tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp"); 198 | } 199 | 200 | #[test] 201 | fn test_vpub_to_electrum() { 202 | let electrum_xpub = ElectrumExtendedPubKey::new( 203 | Xpub::from_str("tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp").unwrap(), 204 | "wpkh".to_string(), 205 | ); 206 | assert_eq!(electrum_xpub.xpub.to_string(),"tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp"); 207 | assert_eq!(electrum_xpub.kind, "wpkh"); 208 | assert_eq!(electrum_xpub.electrum_xpub().unwrap(), "vpub5VXaSncXqxLbdmvrC4Y8z9CszPwuEscADoetWhfrxDFzPUbL5nbVtanYDkrVEutkv9n5A5aCcvRC9swbjDKgHjCZ2tAeae8VsBuPbS8KpXv"); 209 | } 210 | 211 | #[test] 212 | fn test_vpub_roundtrip() { 213 | let elxpub = "vpub5VXaSncXqxLbdmvrC4Y8z9CszPwuEscADoetWhfrxDFzPUbL5nbVtanYDkrVEutkv9n5A5aCcvRC9swbjDKgHjCZ2tAeae8VsBuPbS8KpXv"; 214 | let electrum_xpub = ElectrumExtendedPubKey::from_str(elxpub).unwrap(); 215 | assert_eq!(electrum_xpub.electrum_xpub().unwrap(), elxpub); 216 | assert_ne!(elxpub, electrum_xpub.xpub.to_string()); 217 | } 218 | 219 | #[test] 220 | fn test_slip121_vectors() { 221 | // from https://github.com/satoshilabs/slips/blob/master/slip-0132.md 222 | test_first_address("xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj","1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA"); 223 | test_first_address("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP","37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf"); 224 | test_first_address("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs","bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"); 225 | } 226 | 227 | fn test_first_address(electrum_xpub: &str, expected_first_address: &str) { 228 | let electrum_xpub = ElectrumExtendedPubKey::from_str(electrum_xpub).unwrap(); 229 | assert_eq!(electrum_xpub.xpub.network, Network::Bitcoin.into()); 230 | let descriptors = electrum_xpub.to_descriptors(); 231 | let descriptor: miniscript::Descriptor = 232 | descriptors.external.parse().unwrap(); 233 | let secp = Secp256k1::verification_only(); 234 | let first_address = descriptor 235 | .at_derivation_index(0) 236 | .unwrap() 237 | .derived_descriptor(&secp) 238 | .unwrap() 239 | .address(miniscript::bitcoin::Network::Bitcoin) 240 | .unwrap() 241 | .to_string(); 242 | assert_eq!(expected_first_address, first_address); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/electrum_wallet_file.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Descriptors, Electrum2DescriptorError, ElectrumExtendedKey, ElectrumExtendedPrivKey, 3 | ElectrumExtendedPubKey, 4 | }; 5 | use bitcoin::bip32::{Xpriv, Xpub}; 6 | use regex::Regex; 7 | use serde::{de, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; 8 | use std::{fmt, io::BufReader, path::Path, str::FromStr, string::ToString}; 9 | 10 | /// Representation of an electrum wallet file. Has custom serialization and de-serialization routines to more accurately represent what we need, and the electrum wallet file format. 11 | #[derive(Clone, Debug, PartialEq, Eq)] 12 | pub struct ElectrumWalletFile { 13 | addresses: Addresses, 14 | wallet_type: WalletType, 15 | keystores: Vec, 16 | } 17 | 18 | impl ElectrumWalletFile { 19 | /// Construct a wallet 20 | pub fn new( 21 | keystores: &[Keystore], 22 | min_signatures: u8, 23 | ) -> Result { 24 | let wallet = if keystores.len() == 1 { 25 | ElectrumWalletFile { 26 | addresses: Addresses::new(), 27 | wallet_type: WalletType::Standard, 28 | keystores: keystores.to_vec(), 29 | } 30 | } else if keystores.len() >= 255 { 31 | return Err(Electrum2DescriptorError::TooManyKeyStores(keystores.len())); 32 | } else { 33 | ElectrumWalletFile { 34 | addresses: Addresses::new(), 35 | wallet_type: WalletType::Multisig(min_signatures, keystores.len() as u8), 36 | keystores: keystores.to_vec(), 37 | } 38 | }; 39 | wallet.validate()?; 40 | Ok(wallet) 41 | } 42 | 43 | /// Getter for addresses 44 | pub fn addresses(&self) -> &Addresses { 45 | &self.addresses 46 | } 47 | 48 | /// Getter for wallet_type 49 | pub fn wallet_type(&self) -> &WalletType { 50 | &self.wallet_type 51 | } 52 | 53 | /// Getter for keystores 54 | pub fn keystores(&self) -> &Vec { 55 | &self.keystores 56 | } 57 | 58 | /// Parse an electrum wallet file 59 | pub fn from_file(wallet_file: &Path) -> Result { 60 | let file = std::fs::File::open(wallet_file)?; 61 | let reader = BufReader::new(file); 62 | let wallet = serde_json::from_reader(reader)?; 63 | Ok(wallet) 64 | } 65 | 66 | /// Write to an electrum wallet file 67 | pub fn to_file(&self, wallet_file: &Path) -> Result<(), Electrum2DescriptorError> { 68 | let file = std::fs::File::create(wallet_file)?; 69 | Ok(serde_json::to_writer_pretty(file, self)?) 70 | } 71 | 72 | /// Construct from an output descriptor. Only the external descriptor is needed, the change descriptor is implied. 73 | pub fn from_descriptor(desc: &str) -> Result { 74 | let wallet = if desc.contains("(sortedmulti(") { 75 | ElectrumWalletFile::from_descriptor_multisig(desc) 76 | } else { 77 | ElectrumWalletFile::from_descriptor_singlesig(desc) 78 | }?; 79 | wallet.validate()?; 80 | Ok(wallet) 81 | } 82 | 83 | /// Construct from a single signature output descriptor. Only the external descriptor is needed, the change descriptor is implied. 84 | fn from_descriptor_singlesig(desc: &str) -> Result { 85 | let re = 86 | Regex::new(r#"(pkh|sh\(wpkh|sh\(wsh|wpkh|wsh)\((([tx]p(ub|rv)[0-9A-Za-z]+)/0/\*)\)+"#)?; 87 | let captures = re.captures(desc).map(|captures| { 88 | captures 89 | .iter() 90 | .skip(1) 91 | .take(3) 92 | .flatten() 93 | .map(|c| c.as_str()) 94 | .collect::>() 95 | }); 96 | let keystore = match captures.as_deref() { 97 | Some([kind, _, xkey]) => Keystore::new(kind, xkey)?, 98 | _ => { 99 | return Err(Electrum2DescriptorError::UnknownDescriptorFormat(format!( 100 | "{:?}", 101 | captures 102 | ))) 103 | } 104 | }; 105 | 106 | Ok(ElectrumWalletFile { 107 | addresses: Addresses::new(), 108 | keystores: vec![keystore], 109 | wallet_type: WalletType::Standard, 110 | }) 111 | } 112 | 113 | /// Construct from a multisig output descriptor. Only the external descriptor is needed, the change descriptor is implied. 114 | fn from_descriptor_multisig(desc: &str) -> Result { 115 | let re = Regex::new( 116 | r#"(sh|sh\(wsh|wsh)\(sortedmulti\((\d),([tx]p(ub|rv)[0-9A-Za-z]+/0/\*,?)+\)+"#, 117 | )?; 118 | let captures = re.captures(desc).map(|captures| { 119 | captures 120 | .iter() 121 | .skip(1) 122 | .take(2) 123 | .flatten() 124 | .map(|c| c.as_str()) 125 | .collect::>() 126 | }); 127 | if let Some([kind, x]) = captures.as_deref() { 128 | let kind = match *kind { 129 | "wsh" => "wsh", 130 | "sh" => "pkh", 131 | "sh(wsh" => "sh(wsh", 132 | _ => { 133 | return Err(Electrum2DescriptorError::UnknownScriptKind( 134 | kind.to_string(), 135 | )) 136 | } 137 | }; 138 | let re = Regex::new(r#"[tx]p[ur][bv][0-9A-Za-z]+"#)?; 139 | let keystores = re 140 | .captures_iter(desc) 141 | .map(|cap| Keystore::new(kind, &cap[0])) 142 | .collect::, _>>()?; 143 | let y = keystores.len(); 144 | if y < 2 { 145 | return Err(Electrum2DescriptorError::MultisigFewSigners); 146 | } 147 | 148 | Ok(ElectrumWalletFile { 149 | addresses: Addresses::new(), 150 | keystores, 151 | wallet_type: WalletType::Multisig(x.parse().unwrap(), y as u8), 152 | }) 153 | } else { 154 | Err(Electrum2DescriptorError::UnknownDescriptorFormat(format!( 155 | "{:?}", 156 | captures 157 | ))) 158 | } 159 | } 160 | 161 | /// Generate output descriptors matching the electrum wallet 162 | pub fn to_descriptors(&self) -> Result { 163 | match self.wallet_type { 164 | WalletType::Standard => { 165 | let exkey = self.keystores[0].get_xkey()?; 166 | let desc_ext = exkey.kind().to_string() + "(" + &exkey.xkey_str() + "/0/*)"; 167 | let desc_chg = exkey.kind().to_string() + "(" + &exkey.xkey_str() + "/1/*)"; 168 | 169 | Ok(Descriptors { 170 | external: desc_ext, 171 | change: desc_chg, 172 | }) 173 | } 174 | WalletType::Multisig(x, _y) => { 175 | let xkeys = self 176 | .keystores 177 | .iter() 178 | .map(|ks| ks.get_xkey()) 179 | .collect::>, _>>()?; 180 | let prefix = match xkeys[0].kind() as &str { 181 | "pkh" => "sh", 182 | kind => kind, 183 | } 184 | .to_string(); 185 | let prefix = format!("{}(sortedmulti({}", prefix, x); 186 | 187 | let mut desc = xkeys.iter().fold(prefix, |acc, exkey| { 188 | acc + &(",".to_string() + &exkey.xkey_str() + "/0/*") 189 | }); 190 | desc += "))"; 191 | let opening = desc.matches('(').count(); 192 | let closing = desc.matches(')').count(); 193 | if opening > closing { 194 | desc += ")" 195 | }; 196 | let desc_chg = desc.replace("/0/*", "/1/*"); 197 | 198 | Ok(Descriptors { 199 | external: desc, 200 | change: desc_chg, 201 | }) 202 | } 203 | } 204 | } 205 | 206 | /// validate the internal structure 207 | fn validate(&self) -> Result<(), Electrum2DescriptorError> { 208 | let expected_keystores: usize = match self.wallet_type { 209 | WalletType::Standard => 1, 210 | WalletType::Multisig(_x, y) => y.into(), 211 | }; 212 | 213 | if self.keystores.len() != expected_keystores { 214 | return Err(Electrum2DescriptorError::WrongNumberOfKeyStores( 215 | self.keystores.len(), 216 | expected_keystores, 217 | )); 218 | } 219 | 220 | if let WalletType::Multisig(x, _y) = self.wallet_type { 221 | if x as usize > expected_keystores { 222 | return Err(Electrum2DescriptorError::NumberSignaturesKeyStores( 223 | x, 224 | expected_keystores, 225 | )); 226 | } 227 | } 228 | 229 | Ok(()) 230 | } 231 | } 232 | 233 | impl FromStr for ElectrumWalletFile { 234 | type Err = Electrum2DescriptorError; 235 | 236 | /// Parse an electrum wallet file from string 237 | fn from_str(wallet_file: &str) -> Result { 238 | Ok(serde_json::from_str(wallet_file)?) 239 | } 240 | } 241 | 242 | impl std::fmt::Display for ElectrumWalletFile { 243 | /// Write to a string as electrum wallet file format 244 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 245 | let json = serde_json::to_string_pretty(self).map_err(|_| std::fmt::Error {})?; 246 | write!(f, "{}", json) 247 | } 248 | } 249 | 250 | impl Serialize for ElectrumWalletFile { 251 | fn serialize(&self, serializer: S) -> Result 252 | where 253 | S: Serializer, 254 | { 255 | // We don't know the length of the map at this point, so it's None 256 | let mut map = serializer.serialize_map(None)?; 257 | map.serialize_entry("addresses", &self.addresses)?; 258 | map.serialize_entry("wallet_type", &self.wallet_type)?; 259 | match self.wallet_type { 260 | WalletType::Standard => { 261 | map.serialize_entry("keystore", &self.keystores[0])?; 262 | } 263 | WalletType::Multisig(_x, _y) => { 264 | self.keystores 265 | .iter() 266 | .enumerate() 267 | .map(|(i, keystore)| { 268 | let key = format!("x{}/", i + 1); 269 | map.serialize_entry(&key, &keystore) 270 | }) 271 | .collect::, _>>()?; 272 | } 273 | } 274 | map.end() 275 | } 276 | } 277 | 278 | impl<'de> Deserialize<'de> for ElectrumWalletFile { 279 | fn deserialize(deserializer: D) -> Result 280 | where 281 | D: Deserializer<'de>, 282 | { 283 | enum Field { 284 | Addrs, 285 | Keyst, 286 | WalTyp, 287 | Ignore, 288 | } 289 | 290 | impl<'de> Deserialize<'de> for Field { 291 | fn deserialize(deserializer: D) -> Result 292 | where 293 | D: Deserializer<'de>, 294 | { 295 | struct FieldVisitor; 296 | 297 | impl<'de> de::Visitor<'de> for FieldVisitor { 298 | type Value = Field; 299 | 300 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 301 | formatter.write_str( 302 | "`addresses` or `keystore` or `wallet_type` or 'x1/` or `x2/`", 303 | ) 304 | } 305 | 306 | fn visit_str(self, value: &str) -> Result 307 | where 308 | E: de::Error, 309 | { 310 | let re = Regex::new(r#"(x)(\d+)(/)|([a-z_\-0-9]+)"#).unwrap(); 311 | let captures = re.captures(value).map(|captures| { 312 | captures 313 | .iter() 314 | .skip(1) 315 | .flatten() 316 | .map(|c| c.as_str()) 317 | .collect::>() 318 | }); 319 | match captures.as_deref() { 320 | Some(["x", _i, "/"]) => Ok(Field::Keyst), 321 | Some(["keystore"]) => Ok(Field::Keyst), 322 | Some(["addresses"]) => Ok(Field::Addrs), 323 | Some(["wallet_type"]) => Ok(Field::WalTyp), 324 | _ => Ok(Field::Ignore), 325 | } 326 | } 327 | } 328 | 329 | deserializer.deserialize_identifier(FieldVisitor) 330 | } 331 | } 332 | 333 | struct ElectrumWalletFileVisitor; 334 | 335 | impl<'de> de::Visitor<'de> for ElectrumWalletFileVisitor { 336 | type Value = ElectrumWalletFile; 337 | 338 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 339 | formatter.write_str("struct ElectrumWalletFile") 340 | } 341 | 342 | fn visit_map(self, mut map: V) -> Result 343 | where 344 | V: de::MapAccess<'de>, 345 | { 346 | let mut addresses = Addresses::new(); 347 | let mut keystores = Vec::new(); 348 | let mut wallet_type = WalletType::Standard; 349 | 350 | while let Some(key) = map.next_key()? { 351 | match key { 352 | Field::Addrs => { 353 | addresses = map.next_value()?; 354 | } 355 | Field::Keyst => { 356 | keystores.push(map.next_value()?); 357 | } 358 | Field::WalTyp => { 359 | wallet_type = map.next_value()?; 360 | } 361 | Field::Ignore => { 362 | let _ignore = map.next_value::()?; 363 | } 364 | } 365 | } 366 | 367 | let wallet = ElectrumWalletFile { 368 | addresses, 369 | keystores, 370 | wallet_type, 371 | }; 372 | wallet.validate().map_err(de::Error::custom)?; 373 | Ok(wallet) 374 | } 375 | } 376 | 377 | const FIELDS: &[&str] = &[ 378 | "addresses", 379 | "addr_history", 380 | "channel_backups", 381 | "keystore", 382 | "wallet_type", 383 | "x1/", 384 | "x2/", 385 | "x3/", 386 | ]; 387 | deserializer.deserialize_struct("ElectrumWalletFile", FIELDS, ElectrumWalletFileVisitor) 388 | } 389 | } 390 | 391 | /// Representation of the addresses section of an electrum wallet file 392 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] 393 | pub struct Addresses { 394 | pub change: Vec, 395 | pub receiving: Vec, 396 | } 397 | 398 | impl Addresses { 399 | fn new() -> Self { 400 | Addresses { 401 | change: Vec::new(), 402 | receiving: Vec::new(), 403 | } 404 | } 405 | } 406 | 407 | /// Representation of a keystore section of an electrum wallet file. Can be single sig "keystore" or multisig "x1/" "x2/" ... 408 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] 409 | pub struct Keystore { 410 | #[serde(default = "Keystore::default_type")] 411 | pub r#type: String, 412 | pub xprv: Option, 413 | pub xpub: String, 414 | } 415 | 416 | impl Keystore { 417 | /// Construct a Keystore from script kind and xpub or xprv 418 | fn new(kind: &str, xkey: &str) -> Result { 419 | let xprv = Xpriv::from_str(xkey); 420 | let exprv = if let Ok(xprv) = xprv { 421 | Some(ElectrumExtendedPrivKey::new(xprv, kind.to_string()).electrum_xprv()?) 422 | } else { 423 | None 424 | }; 425 | 426 | let expub = if let Ok(xprv) = xprv { 427 | let secp = bitcoin::secp256k1::Secp256k1::new(); 428 | ElectrumExtendedPubKey::new(Xpub::from_priv(&secp, &xprv), kind.to_string()) 429 | } else { 430 | ElectrumExtendedPubKey::new(Xpub::from_str(xkey)?, kind.to_string()) 431 | } 432 | .electrum_xpub()?; 433 | 434 | Ok(Keystore { 435 | r#type: Keystore::default_type(), 436 | xprv: exprv, 437 | xpub: expub, 438 | }) 439 | } 440 | 441 | /// Get the xprv if available or else the xpub. 442 | fn get_xkey(&self) -> Result, Electrum2DescriptorError> { 443 | if let Some(xprv) = &self.xprv { 444 | let exprv = ElectrumExtendedPrivKey::from_str(xprv)?; 445 | return Ok(Box::new(exprv)); 446 | } 447 | 448 | let expub = ElectrumExtendedPubKey::from_str(&self.xpub)?; 449 | Ok(Box::new(expub)) 450 | } 451 | 452 | /// Default keystore type to use if nothing else was specified 453 | fn default_type() -> String { 454 | "bip32".to_string() 455 | } 456 | } 457 | 458 | /// Representation of the wallet_type section of an electrum wallet file. Has custom serialization and de-serialization implementatoin. 459 | #[derive(Clone, Debug, PartialEq, Eq)] 460 | pub enum WalletType { 461 | Standard, 462 | Multisig(u8, u8), 463 | } 464 | 465 | impl fmt::Display for WalletType { 466 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 467 | write!(f, "{:?}", self) 468 | } 469 | } 470 | 471 | impl FromStr for WalletType { 472 | type Err = Electrum2DescriptorError; 473 | 474 | /// Parse WalletType from a string representation 475 | fn from_str(wallet_type: &str) -> Result { 476 | let re = Regex::new(r#"(standard)|(\d+)(of)(\d+)"#)?; 477 | let captures = re.captures(wallet_type).map(|captures| { 478 | captures 479 | .iter() 480 | .skip(1) 481 | .flatten() 482 | .map(|c| c.as_str()) 483 | .collect::>() 484 | }); 485 | match captures.as_deref() { 486 | Some(["standard"]) => Ok(WalletType::Standard), 487 | Some([x, "of", y]) => Ok(WalletType::Multisig(x.parse().unwrap(), y.parse().unwrap())), 488 | _ => Err(Electrum2DescriptorError::UnknownWalletType( 489 | wallet_type.to_string(), 490 | )), 491 | } 492 | } 493 | } 494 | 495 | impl<'de> Deserialize<'de> for WalletType { 496 | fn deserialize(deserializer: D) -> Result 497 | where 498 | D: Deserializer<'de>, 499 | { 500 | let s = String::deserialize(deserializer)?; 501 | WalletType::from_str(&s).map_err(de::Error::custom) 502 | } 503 | } 504 | 505 | impl Serialize for WalletType { 506 | fn serialize(&self, serializer: S) -> Result 507 | where 508 | S: serde::Serializer, 509 | { 510 | let s = match *self { 511 | WalletType::Standard => "standard".to_string(), 512 | WalletType::Multisig(x, y) => format!("{}of{}", x, y), 513 | }; 514 | serializer.serialize_str(&s) 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::{base58, bip32, secp256k1}; 2 | #[cfg(feature = "wallet_file")] 3 | use serde_json::Error as SerdeError; 4 | use std::io; 5 | use thiserror::Error; 6 | 7 | #[derive(Error, Debug)] 8 | pub enum Electrum2DescriptorError { 9 | #[cfg(feature = "wallet_file")] 10 | #[error(transparent)] 11 | Serde(#[from] SerdeError), 12 | #[error(transparent)] 13 | IO(#[from] io::Error), 14 | #[error(transparent)] 15 | Infallible(#[from] std::convert::Infallible), 16 | #[error(transparent)] 17 | Base58Error(#[from] base58::Error), 18 | #[error(transparent)] 19 | Secp256k1Error(#[from] secp256k1::Error), 20 | #[error(transparent)] 21 | Bip32Error(#[from] bip32::Error), 22 | #[cfg(feature = "wallet_file")] 23 | #[error(transparent)] 24 | RegexError(#[from] regex::Error), 25 | 26 | #[error("Unknown type")] 27 | UnknownType, 28 | #[error("Unknown wallet type: {0}")] 29 | UnknownWalletType(String), 30 | #[error("Multisig with less than two signers doesn't make a lot of sense")] 31 | MultisigFewSigners, 32 | #[error("Unknown multisig descriptor format: {0}")] 33 | UnknownDescriptorFormat(String), 34 | #[error("Wrong number of keystores: {0}; expected: {1}")] 35 | WrongNumberOfKeyStores(usize, usize), 36 | #[error("Minimum number of signatures {0} must not be greater than keystores {1}")] 37 | NumberSignaturesKeyStores(u8, usize), 38 | #[error("keystore sizes above 255 are not currently supported. {0}")] 39 | TooManyKeyStores(usize), 40 | #[error("Unknown script kind: {0}")] 41 | UnknownScriptKind(String), 42 | #[error("Incorrect length of string representation: {0}")] 43 | InvalidLength(usize), 44 | #[error("Unknown sentinel")] 45 | InvalidExtendedKeyVersion([u8; 4]), 46 | #[error("{0}")] 47 | GenericBorrow(&'static str), 48 | } 49 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod electrum_extended_priv_key; 2 | pub mod electrum_extended_pub_key; 3 | #[cfg(feature = "wallet_file")] 4 | pub mod electrum_wallet_file; 5 | pub mod errors; 6 | 7 | pub use electrum_extended_priv_key::ElectrumExtendedPrivKey; 8 | pub use electrum_extended_pub_key::ElectrumExtendedPubKey; 9 | #[cfg(feature = "wallet_file")] 10 | pub use electrum_wallet_file::ElectrumWalletFile; 11 | pub use errors::Electrum2DescriptorError; 12 | 13 | pub trait ElectrumExtendedKey { 14 | /// Returns internal and external descriptor 15 | fn to_descriptors(&self) -> Descriptors; 16 | 17 | /// Returns the bitcoin extended key (xpub or xprv) as String 18 | fn xkey_str(&self) -> String; 19 | 20 | /// Returns the kind of script 21 | fn kind(&self) -> &str; 22 | } 23 | 24 | /// The two descriptors for external and change addresses 25 | #[derive(Clone, Debug, PartialEq, Eq)] 26 | pub struct Descriptors { 27 | pub external: String, 28 | pub change: String, 29 | } 30 | -------------------------------------------------------------------------------- /tests/wallets/default_legacy: -------------------------------------------------------------------------------- 1 | { 2 | "addr_history": { 3 | "mfgj7Gt1du65VjhheX9JmoWJFFrC3T6eN4": [], 4 | "mgeDvD5RuRkor5ksTqfN1DRCGFBGvUj5UP": [], 5 | "mij2XBWb846HPyvpyj7axZjea4Y1uWAQ8T": [], 6 | "mir61H4NHJM9oxvLxVfsGaTUk8UnsHoEms": [], 7 | "mjRfzVeJ3wzMe6H1wE6BLgqA95HeusFfrS": [], 8 | "mkdftU7nSvaBAJ4ZnBNndGTipmgspty4S4": [], 9 | "mmsZKnu4R1WfxnjRmhEwGFGAEtCNwQLGjD": [], 10 | "mn3iqsQmbrQmdmpNKbPQosb6PrPkTnb2iD": [], 11 | "moJnNAQRBZcHFukZzssBPFQTvnJEM4sTah": [], 12 | "mps9ghJWYvt8BDeF5Up8VJLfdHbs8nJYie": [], 13 | "mpt1NVkN8h8Tm8Mv1kmudZTTJorGAMpBJA": [], 14 | "mq11A2HdA2uS1mf7M5e3FdNCfYtCY938st": [], 15 | "mqexdw4S3guJJXMLvRPTe7f9H8X2Vq1bEG": [], 16 | "mqrqZq7tMW8uZKbMoeWgZ16N88WpGZukvB": [], 17 | "mqwAY9CkZXYjYUGGYdU2QPaDQdZocg9G2N": [], 18 | "mqyVF3b2CSZqWuX4WhDdJH9cC8ND51MG6R": [], 19 | "msbPosJpVTwzRyBVUDcF5eaRARTNKZ8ysx": [], 20 | "mtxNDguhDCgSDRhUnVKWayagKmT8fGaxT8": [], 21 | "muEPi32UxzkTMu3Ugf9g2a7w4RHJeAHWM6": [], 22 | "muVsjryQ7q1YvQ9Ji7MqrxNAnSSMccDYeS": [], 23 | "muzkqevZm9Mr4uNPCtj6Jr8YZvz58M9QFS": [], 24 | "mw6EKJaKBsBFRGqTbUCtEb4R9r4WQGykQn": [], 25 | "mwbwnXJDvttmmG8kcizahn5buaCb98emBC": [], 26 | "mwdbw6f3pFMXRuq1zERSKmPbdsFoJCnFF3": [], 27 | "mxFNU8bhghhdxbxv77JzkUrjMYPzQR6ncq": [], 28 | "mzeUYrE1VXtAzAxJ81ehg2eN43d2Xx7opn": [], 29 | "n1sNWgkLAHmEDr498EVTfGvwbstdkJDzvf": [], 30 | "n233ZDynrCajKgcD2RH8WoeK27BBwzAn2n": [], 31 | "n3M4ZzJNMdmDhFJiwgUGgVtvQEvcwuMW26": [], 32 | "n3w3EHgaX11VwzWbjLx8bYoGHNurV4JFGH": [] 33 | }, 34 | "addresses": { 35 | "change": [ 36 | "mmsZKnu4R1WfxnjRmhEwGFGAEtCNwQLGjD", 37 | "mwbwnXJDvttmmG8kcizahn5buaCb98emBC", 38 | "mgeDvD5RuRkor5ksTqfN1DRCGFBGvUj5UP", 39 | "mtxNDguhDCgSDRhUnVKWayagKmT8fGaxT8", 40 | "mqyVF3b2CSZqWuX4WhDdJH9cC8ND51MG6R", 41 | "mqwAY9CkZXYjYUGGYdU2QPaDQdZocg9G2N", 42 | "muEPi32UxzkTMu3Ugf9g2a7w4RHJeAHWM6", 43 | "mij2XBWb846HPyvpyj7axZjea4Y1uWAQ8T", 44 | "mwdbw6f3pFMXRuq1zERSKmPbdsFoJCnFF3", 45 | "mq11A2HdA2uS1mf7M5e3FdNCfYtCY938st" 46 | ], 47 | "receiving": [ 48 | "muVsjryQ7q1YvQ9Ji7MqrxNAnSSMccDYeS", 49 | "n3w3EHgaX11VwzWbjLx8bYoGHNurV4JFGH", 50 | "mfgj7Gt1du65VjhheX9JmoWJFFrC3T6eN4", 51 | "mqexdw4S3guJJXMLvRPTe7f9H8X2Vq1bEG", 52 | "mxFNU8bhghhdxbxv77JzkUrjMYPzQR6ncq", 53 | "mqrqZq7tMW8uZKbMoeWgZ16N88WpGZukvB", 54 | "muzkqevZm9Mr4uNPCtj6Jr8YZvz58M9QFS", 55 | "mzeUYrE1VXtAzAxJ81ehg2eN43d2Xx7opn", 56 | "mjRfzVeJ3wzMe6H1wE6BLgqA95HeusFfrS", 57 | "n1sNWgkLAHmEDr498EVTfGvwbstdkJDzvf", 58 | "mn3iqsQmbrQmdmpNKbPQosb6PrPkTnb2iD", 59 | "mpt1NVkN8h8Tm8Mv1kmudZTTJorGAMpBJA", 60 | "mkdftU7nSvaBAJ4ZnBNndGTipmgspty4S4", 61 | "msbPosJpVTwzRyBVUDcF5eaRARTNKZ8ysx", 62 | "moJnNAQRBZcHFukZzssBPFQTvnJEM4sTah", 63 | "n233ZDynrCajKgcD2RH8WoeK27BBwzAn2n", 64 | "mw6EKJaKBsBFRGqTbUCtEb4R9r4WQGykQn", 65 | "mir61H4NHJM9oxvLxVfsGaTUk8UnsHoEms", 66 | "n3M4ZzJNMdmDhFJiwgUGgVtvQEvcwuMW26", 67 | "mps9ghJWYvt8BDeF5Up8VJLfdHbs8nJYie" 68 | ] 69 | }, 70 | "channel_backups": {}, 71 | "fiat_value": {}, 72 | "invoices": {}, 73 | "keystore": { 74 | "derivation": "m", 75 | "pw_hash_version": 1, 76 | "root_fingerprint": "230b70d2", 77 | "seed": "mail high accident nothing immune blanket suggest donor number gravity palm lamp", 78 | "type": "bip32", 79 | "xprv": "tprv8ZgxMBicQKsPeYnCHtn5QZqhTgkkDmXebfQMXWmX7ThXJFCbzDTKFNRsB43GUmHzu2pdGcnnegFy175kFcgZQYC5BFPnRdYDPQyqetpyjb5", 80 | "xpub": "tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt" 81 | }, 82 | "labels": {}, 83 | "payment_requests": {}, 84 | "prevouts_by_scripthash": {}, 85 | "seed_type": "standard", 86 | "seed_version": 33, 87 | "spent_outpoints": {}, 88 | "transactions": {}, 89 | "tx_fees": {}, 90 | "txi": {}, 91 | "txo": {}, 92 | "use_encryption": false, 93 | "verified_tx3": {}, 94 | "wallet_type": "standard" 95 | } -------------------------------------------------------------------------------- /tests/wallets/default_legacy_watch: -------------------------------------------------------------------------------- 1 | { 2 | "addr_history": { 3 | "mfgj7Gt1du65VjhheX9JmoWJFFrC3T6eN4": [], 4 | "mgeDvD5RuRkor5ksTqfN1DRCGFBGvUj5UP": [], 5 | "mij2XBWb846HPyvpyj7axZjea4Y1uWAQ8T": [], 6 | "mir61H4NHJM9oxvLxVfsGaTUk8UnsHoEms": [], 7 | "mjRfzVeJ3wzMe6H1wE6BLgqA95HeusFfrS": [], 8 | "mkdftU7nSvaBAJ4ZnBNndGTipmgspty4S4": [], 9 | "mmsZKnu4R1WfxnjRmhEwGFGAEtCNwQLGjD": [], 10 | "mn3iqsQmbrQmdmpNKbPQosb6PrPkTnb2iD": [], 11 | "moJnNAQRBZcHFukZzssBPFQTvnJEM4sTah": [], 12 | "mps9ghJWYvt8BDeF5Up8VJLfdHbs8nJYie": [], 13 | "mpt1NVkN8h8Tm8Mv1kmudZTTJorGAMpBJA": [], 14 | "mq11A2HdA2uS1mf7M5e3FdNCfYtCY938st": [], 15 | "mqexdw4S3guJJXMLvRPTe7f9H8X2Vq1bEG": [], 16 | "mqrqZq7tMW8uZKbMoeWgZ16N88WpGZukvB": [], 17 | "mqwAY9CkZXYjYUGGYdU2QPaDQdZocg9G2N": [], 18 | "mqyVF3b2CSZqWuX4WhDdJH9cC8ND51MG6R": [], 19 | "msbPosJpVTwzRyBVUDcF5eaRARTNKZ8ysx": [], 20 | "mtxNDguhDCgSDRhUnVKWayagKmT8fGaxT8": [], 21 | "muEPi32UxzkTMu3Ugf9g2a7w4RHJeAHWM6": [], 22 | "muVsjryQ7q1YvQ9Ji7MqrxNAnSSMccDYeS": [], 23 | "muzkqevZm9Mr4uNPCtj6Jr8YZvz58M9QFS": [], 24 | "mw6EKJaKBsBFRGqTbUCtEb4R9r4WQGykQn": [], 25 | "mwbwnXJDvttmmG8kcizahn5buaCb98emBC": [], 26 | "mwdbw6f3pFMXRuq1zERSKmPbdsFoJCnFF3": [], 27 | "mxFNU8bhghhdxbxv77JzkUrjMYPzQR6ncq": [], 28 | "mzeUYrE1VXtAzAxJ81ehg2eN43d2Xx7opn": [], 29 | "n1sNWgkLAHmEDr498EVTfGvwbstdkJDzvf": [], 30 | "n233ZDynrCajKgcD2RH8WoeK27BBwzAn2n": [], 31 | "n3M4ZzJNMdmDhFJiwgUGgVtvQEvcwuMW26": [], 32 | "n3w3EHgaX11VwzWbjLx8bYoGHNurV4JFGH": [] 33 | }, 34 | "addresses": { 35 | "change": [ 36 | "mmsZKnu4R1WfxnjRmhEwGFGAEtCNwQLGjD", 37 | "mwbwnXJDvttmmG8kcizahn5buaCb98emBC", 38 | "mgeDvD5RuRkor5ksTqfN1DRCGFBGvUj5UP", 39 | "mtxNDguhDCgSDRhUnVKWayagKmT8fGaxT8", 40 | "mqyVF3b2CSZqWuX4WhDdJH9cC8ND51MG6R", 41 | "mqwAY9CkZXYjYUGGYdU2QPaDQdZocg9G2N", 42 | "muEPi32UxzkTMu3Ugf9g2a7w4RHJeAHWM6", 43 | "mij2XBWb846HPyvpyj7axZjea4Y1uWAQ8T", 44 | "mwdbw6f3pFMXRuq1zERSKmPbdsFoJCnFF3", 45 | "mq11A2HdA2uS1mf7M5e3FdNCfYtCY938st" 46 | ], 47 | "receiving": [ 48 | "muVsjryQ7q1YvQ9Ji7MqrxNAnSSMccDYeS", 49 | "n3w3EHgaX11VwzWbjLx8bYoGHNurV4JFGH", 50 | "mfgj7Gt1du65VjhheX9JmoWJFFrC3T6eN4", 51 | "mqexdw4S3guJJXMLvRPTe7f9H8X2Vq1bEG", 52 | "mxFNU8bhghhdxbxv77JzkUrjMYPzQR6ncq", 53 | "mqrqZq7tMW8uZKbMoeWgZ16N88WpGZukvB", 54 | "muzkqevZm9Mr4uNPCtj6Jr8YZvz58M9QFS", 55 | "mzeUYrE1VXtAzAxJ81ehg2eN43d2Xx7opn", 56 | "mjRfzVeJ3wzMe6H1wE6BLgqA95HeusFfrS", 57 | "n1sNWgkLAHmEDr498EVTfGvwbstdkJDzvf", 58 | "mn3iqsQmbrQmdmpNKbPQosb6PrPkTnb2iD", 59 | "mpt1NVkN8h8Tm8Mv1kmudZTTJorGAMpBJA", 60 | "mkdftU7nSvaBAJ4ZnBNndGTipmgspty4S4", 61 | "msbPosJpVTwzRyBVUDcF5eaRARTNKZ8ysx", 62 | "moJnNAQRBZcHFukZzssBPFQTvnJEM4sTah", 63 | "n233ZDynrCajKgcD2RH8WoeK27BBwzAn2n", 64 | "mw6EKJaKBsBFRGqTbUCtEb4R9r4WQGykQn", 65 | "mir61H4NHJM9oxvLxVfsGaTUk8UnsHoEms", 66 | "n3M4ZzJNMdmDhFJiwgUGgVtvQEvcwuMW26", 67 | "mps9ghJWYvt8BDeF5Up8VJLfdHbs8nJYie" 68 | ] 69 | }, 70 | "channel_backups": {}, 71 | "fiat_value": {}, 72 | "invoices": {}, 73 | "keystore": { 74 | "derivation": "m", 75 | "pw_hash_version": 1, 76 | "root_fingerprint": "230b70d2", 77 | "type": "bip32", 78 | "xprv": null, 79 | "xpub": "tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt" 80 | }, 81 | "labels": {}, 82 | "payment_requests": {}, 83 | "prevouts_by_scripthash": {}, 84 | "seed_version": 33, 85 | "spent_outpoints": {}, 86 | "transactions": {}, 87 | "tx_fees": {}, 88 | "txi": {}, 89 | "txo": {}, 90 | "use_encryption": false, 91 | "verified_tx3": {}, 92 | "wallet_type": "standard" 93 | } -------------------------------------------------------------------------------- /tests/wallets/default_segwit: -------------------------------------------------------------------------------- 1 | { 2 | "addr_history": { 3 | "tb1q00ugaxp37gwkd2qrpjdsm9gf3pyt6a00ehncvf": [], 4 | "tb1q0s58y3yagchg20xj33cnphp7c0z334k8xagu9w": [], 5 | "tb1q2fdzrgyffwwhjqd2taa0005w3up3dd3vgx9c3x": [], 6 | "tb1q30nr5r6ye55w9trcew8yujxtssqx6e4pwwd9q9": [], 7 | "tb1q64h5knh9v2q0334y50hq5aenfn7220m8fpsylw": [], 8 | "tb1q7prnnyhzhv4ayussk65zh9f5796d3qysp35mv9": [], 9 | "tb1q8mq3urd3cw3yzfysfla8qa0t4k4nl8jxl9xnen": [], 10 | "tb1q9fu34e296xj6j4hs8r2gus3j2pcsetrcrns0hl": [], 11 | "tb1qcfkdqtagua076xpc46322czem40jtp8ps8uqhw": [], 12 | "tb1qcvs7styhugdey8s3c7c2u5v00er3rp4auk0u69": [], 13 | "tb1qcxwdanhxvuwpwg0hkj4n5j6fzvtxkngdkguca2": [], 14 | "tb1qfud5qkjjve3za0d3gpz2g8qvphcp77gp4kdype": [], 15 | "tb1qg0ugc44dk79rc9d6p4d2lh574c3y29cjrecsee": [], 16 | "tb1qk7mthm5tn5vp3mv6tqrle7fe4ccqutx8stev3s": [], 17 | "tb1qlfaf57qrr7cmlfxxng4mml070ldsek3z8gdzf5": [], 18 | "tb1qmee32rz3mwv090mwqclvh9njlh626e4ztspsa6": [], 19 | "tb1qmekaq8lgunhzzr7m9hvse5kf5lyj003d9sm2nh": [], 20 | "tb1qnd2lyh89dzkdjaevq4vgfqlmhqp390wn09p2nw": [], 21 | "tb1qnlyqzt2nrq6rejy6ldwuu6qn72exxvvfvrdtph": [], 22 | "tb1qnpekfd4dx9dj0xajtpns77lxc949p9f8d67l6x": [], 23 | "tb1qpyznt8yf24jwev6dqks80g03pehw8f8rnlfrf3": [], 24 | "tb1qrysqjfpm8997qdpwlnasx0vhk27t09d8658af2": [], 25 | "tb1qsfs2yq2j46v89u09tznxuqt6dyn0ute6q7z0ql": [], 26 | "tb1qt63km8fkfuwnl6hzfktu7g2nh8tc6jhr6vequq": [], 27 | "tb1qtr7rwgtusg6h3wtzn9r5nenjhyg6rwtmr9qptx": [], 28 | "tb1qw6ph68psgmrgu26hywj7wfrc6fnpvas4dezwgc": [], 29 | "tb1qwruhgyqu34gpsasl7k5dc8zvw57yjkgqz7egvn": [], 30 | "tb1qy2ja9vgjz7xtrl9ks4qnqhvd6fp20ml0q5jmnr": [], 31 | "tb1qyjh7dyaal8yhfamuwt6fngly520w8wzlp7gksh": [], 32 | "tb1qys5xz7kju4k86wau7rusgjqgtf08nxe57hus7c": [] 33 | }, 34 | "addresses": { 35 | "change": [ 36 | "tb1qcvs7styhugdey8s3c7c2u5v00er3rp4auk0u69", 37 | "tb1q8mq3urd3cw3yzfysfla8qa0t4k4nl8jxl9xnen", 38 | "tb1qy2ja9vgjz7xtrl9ks4qnqhvd6fp20ml0q5jmnr", 39 | "tb1qlfaf57qrr7cmlfxxng4mml070ldsek3z8gdzf5", 40 | "tb1qpyznt8yf24jwev6dqks80g03pehw8f8rnlfrf3", 41 | "tb1qcfkdqtagua076xpc46322czem40jtp8ps8uqhw", 42 | "tb1q9fu34e296xj6j4hs8r2gus3j2pcsetrcrns0hl", 43 | "tb1qnpekfd4dx9dj0xajtpns77lxc949p9f8d67l6x", 44 | "tb1qrysqjfpm8997qdpwlnasx0vhk27t09d8658af2", 45 | "tb1q0s58y3yagchg20xj33cnphp7c0z334k8xagu9w" 46 | ], 47 | "receiving": [ 48 | "tb1qt63km8fkfuwnl6hzfktu7g2nh8tc6jhr6vequq", 49 | "tb1qnlyqzt2nrq6rejy6ldwuu6qn72exxvvfvrdtph", 50 | "tb1qys5xz7kju4k86wau7rusgjqgtf08nxe57hus7c", 51 | "tb1q7prnnyhzhv4ayussk65zh9f5796d3qysp35mv9", 52 | "tb1qwruhgyqu34gpsasl7k5dc8zvw57yjkgqz7egvn", 53 | "tb1qk7mthm5tn5vp3mv6tqrle7fe4ccqutx8stev3s", 54 | "tb1q64h5knh9v2q0334y50hq5aenfn7220m8fpsylw", 55 | "tb1qfud5qkjjve3za0d3gpz2g8qvphcp77gp4kdype", 56 | "tb1qmee32rz3mwv090mwqclvh9njlh626e4ztspsa6", 57 | "tb1q2fdzrgyffwwhjqd2taa0005w3up3dd3vgx9c3x", 58 | "tb1qsfs2yq2j46v89u09tznxuqt6dyn0ute6q7z0ql", 59 | "tb1qw6ph68psgmrgu26hywj7wfrc6fnpvas4dezwgc", 60 | "tb1qg0ugc44dk79rc9d6p4d2lh574c3y29cjrecsee", 61 | "tb1qyjh7dyaal8yhfamuwt6fngly520w8wzlp7gksh", 62 | "tb1qmekaq8lgunhzzr7m9hvse5kf5lyj003d9sm2nh", 63 | "tb1q30nr5r6ye55w9trcew8yujxtssqx6e4pwwd9q9", 64 | "tb1qcxwdanhxvuwpwg0hkj4n5j6fzvtxkngdkguca2", 65 | "tb1qtr7rwgtusg6h3wtzn9r5nenjhyg6rwtmr9qptx", 66 | "tb1qnd2lyh89dzkdjaevq4vgfqlmhqp390wn09p2nw", 67 | "tb1q00ugaxp37gwkd2qrpjdsm9gf3pyt6a00ehncvf" 68 | ] 69 | }, 70 | "channel_backups": {}, 71 | "channels": {}, 72 | "fiat_value": {}, 73 | "invoices": {}, 74 | "keystore": { 75 | "derivation": "m/0'", 76 | "pw_hash_version": 1, 77 | "root_fingerprint": "b88448fb", 78 | "seed": "old desert genius anchor vessel kingdom mushroom put rail inspire file biology", 79 | "type": "bip32", 80 | "xprv": "vprv9GbHBLHzHXCCMqbpwrdLBVD4C3zZyUGXJy9bWHGDBvWi5pWhwKVm4a92c2tvAAXmTUxjv7EXz4eMDtb4nzLgYnB9s3kgzwBoEkTWgzGt27g", 81 | "xpub": "vpub5Vadaqpt7tkVaKgJ3tALYd9nk5q4NvzNgC5CJffpkG3gxcqrUrp1cNTWTLaQvXdgL9YPKjJ7btKrJHBW2DScYoFqaoysBhhqNoJXv15W6yr" 82 | }, 83 | "labels": {}, 84 | "lightning_payments": {}, 85 | "lightning_preimages": {}, 86 | "lightning_privkey2": "tprv8ZgxMBicQKsPd7b5oJo6jWkta2BhMT12HCad9a4yDeknu135aRD1vDEBzNpwsjz3SZV5ezNimep6Zdg6Dk3uPe4bRxA5bTfBd1fRXnKymBf", 87 | "payment_requests": {}, 88 | "prevouts_by_scripthash": {}, 89 | "qt-console-history": [], 90 | "seed_type": "segwit", 91 | "seed_version": 33, 92 | "spent_outpoints": {}, 93 | "stored_height": 2134482, 94 | "submarine_swaps": {}, 95 | "transactions": {}, 96 | "tx_fees": {}, 97 | "txi": {}, 98 | "txo": {}, 99 | "use_encryption": false, 100 | "verified_tx3": {}, 101 | "wallet_type": "standard", 102 | "winpos-qt": [ 103 | 1359, 104 | 193, 105 | 851, 106 | 468 107 | ] 108 | } -------------------------------------------------------------------------------- /tests/wallets/imported_addr: -------------------------------------------------------------------------------- 1 | { 2 | "addr_history": {}, 3 | "addresses": { 4 | "tb1q6x5sfksrqg20668q0vj3phfjkrqkzx62ke3mutflxhsca4gjc48qgeusmx": {}, 5 | "tb1q885t7wwtky4djyahs4vscqdgelq4z9yfky9es5fexehh86pzdhws2hhz0x": {}, 6 | "tb1q8mrzmsay59eln9pdvfvae7mevwfxme7v86s9y7q5vgqx5aajwrzsjylq6v": {}, 7 | "tb1q9a4yc2vwep2xefphhedevxz4uj0x5mjj0y0p72cta8eytlghg0lsk9ajf8": {}, 8 | "tb1qazjtd5kxjztpqslt25y3h2nz3pm49yh8jjax6teav4ct7ptjtypssex05e": {}, 9 | "tb1ql0h4d90l8wxfuccv008cl37458e8alfrn35x53rgkqzzg0f672fqms35mw": {}, 10 | "tb1qltttpv04grw8e255w8ksuypn5ak45xvn8zt45xwtmj6ttymyx47sdc3h6q": {}, 11 | "tb1qna8sx3r037vv24yplumshj8qs6c4aps3pqltr0v6c0p2mhwgmmysm5fz2z": {}, 12 | "tb1qver6atmpu2q8nrfr53ywtxlnp3va9w2r3fqw5lha6n4t00hwk7wqwwyphd": {}, 13 | "tb1qxna4v45xnf0j76z7cuetlpx3qdzuk2333f3anm9qymmj6u6w9drq4zzqm2": {}, 14 | "tb1qywdkvysahamjzl9pa0wlssmpplyhm2cqasqznhk7pz4x58sh3x7s7yfkea": {} 15 | }, 16 | "channel_backups": {}, 17 | "fiat_value": {}, 18 | "invoices": {}, 19 | "labels": {}, 20 | "payment_requests": {}, 21 | "prevouts_by_scripthash": {}, 22 | "seed_version": 33, 23 | "spent_outpoints": {}, 24 | "transactions": {}, 25 | "tx_fees": {}, 26 | "txi": {}, 27 | "txo": {}, 28 | "use_encryption": false, 29 | "verified_tx3": {}, 30 | "wallet_type": "imported" 31 | } -------------------------------------------------------------------------------- /tests/wallets/imported_privkey: -------------------------------------------------------------------------------- 1 | { 2 | "addr_history": {}, 3 | "addresses": { 4 | "tb1q7prnnyhzhv4ayussk65zh9f5796d3qysp35mv9": { 5 | "pubkey": "033087391825f0ae207fdc8c9cf17c10c37300b52af728ab2efb274aac45f561af", 6 | "type": "p2wpkh" 7 | }, 8 | "tb1qnlyqzt2nrq6rejy6ldwuu6qn72exxvvfvrdtph": { 9 | "pubkey": "025a52c10e9b6157e66487ed4c5affa324157d870923a57caa8dee673269a63d2d", 10 | "type": "p2wpkh" 11 | }, 12 | "tb1qt63km8fkfuwnl6hzfktu7g2nh8tc6jhr6vequq": { 13 | "pubkey": "0306ecf470a5c8e436187db466e5b84b28267f1b269906e70ef3ba5d134339c689", 14 | "type": "p2wpkh" 15 | }, 16 | "tb1qys5xz7kju4k86wau7rusgjqgtf08nxe57hus7c": { 17 | "pubkey": "03bf483d038037dbaabf3ecb94a10e725de34c8e64676da562a612d35828eb4252", 18 | "type": "p2wpkh" 19 | } 20 | }, 21 | "channel_backups": {}, 22 | "fiat_value": {}, 23 | "invoices": {}, 24 | "keystore": { 25 | "keypairs": { 26 | "025a52c10e9b6157e66487ed4c5affa324157d870923a57caa8dee673269a63d2d": "cXdYHE11WxYPUfNiCaLrySytAbhpLPvxN9zJEHfypTkmR1qEkiYg", 27 | "0306ecf470a5c8e436187db466e5b84b28267f1b269906e70ef3ba5d134339c689": "cee8Lz4767oSuhBTMuaLsjjN6CiqLyke2rkpwqVMcGXw8T2fhzo7", 28 | "033087391825f0ae207fdc8c9cf17c10c37300b52af728ab2efb274aac45f561af": "cZZuMP57XLeLyMtfEBsqVhY7scBcVNW929unVH7WHVheMY25woDp", 29 | "03bf483d038037dbaabf3ecb94a10e725de34c8e64676da562a612d35828eb4252": "cZQzRwivLg3xBSGy75LmNAmCgquBBPouH5zq3z8Z69iNqpCo5hTQ" 30 | }, 31 | "pw_hash_version": 1, 32 | "type": "imported" 33 | }, 34 | "labels": {}, 35 | "payment_requests": {}, 36 | "prevouts_by_scripthash": {}, 37 | "seed_version": 33, 38 | "spent_outpoints": {}, 39 | "transactions": {}, 40 | "tx_fees": {}, 41 | "txi": {}, 42 | "txo": {}, 43 | "use_encryption": false, 44 | "verified_tx3": {}, 45 | "wallet_type": "imported" 46 | } -------------------------------------------------------------------------------- /tests/wallets/multisig_hw_segwit: -------------------------------------------------------------------------------- 1 | { 2 | "addr_history": { 3 | "tb1q4dhcva207vq225ljqfht0xr3ydfmcjj9mcnn9rqc322fpndehhqq9d596z": [], 4 | "tb1q4n590s3wlqtsz4g6jzmfc8whn0fvun3vmjzpuj5740gr56jfjy2qpglrkv": [], 5 | "tb1q6cdpn4d373nnkvn682s37dsyhvy3y6lksdqpuzr9k3ch72mxmhzsa8ed32": [], 6 | "tb1q8av3me42vpffyad9k98z69mpv2k9yw85p8qj0az9mk8scw2mu05sldqaak": [], 7 | "tb1qc7m5fnvnwt9xyng9grcwq6yjmegcxzcxc7hud08cukdlmc43mz3qq56f03": [], 8 | "tb1qcydf4ax86d8z7qf4tktta08c6ulwccuptwtg2gqt22cyq07j40rsenp7zs": [], 9 | "tb1qd9nlskrzekuc582p5e7qkd7aghaf4gqku602pn2c5lez2krdf6qsrtay0x": [], 10 | "tb1qdm29t5pe2ar86d3vz956u8nme425ftq9hwtax7nrngg7ukhfnqjqsn8wnk": [], 11 | "tb1qej6rrdlthzv445r2sar4dku8t6wm0294wwu75s264qes40w3d89qj2gn7q": [], 12 | "tb1qejz6wfrq909mgqe2qla0exdux544xvy6afrntl24wqe349t3jw4s3fxv79": [], 13 | "tb1qets9smu25enexn4r66el43e473jgv42vmtwm52mc3xc9knp2wmes48skss": [], 14 | "tb1qfr3gvv4p67l5zahnppqzjv3fd30c6a5gf7u74r7hyv6snsz6usvszyq40n": [], 15 | "tb1qg2cdtu7c860qgm894l65m4drzpvhskmvsx8cr6h7c72gmmeys6aqvj8cya": [], 16 | "tb1qgpgwsd47hud3l288283l3ndg37kqfgas9t6eeh6h6a7kaek3lzsswfk9le": [], 17 | "tb1qk2cferyakf9rey9gpjmgt233ryk3v6tca26ywqmzmyarahha8s9q6yveda": [], 18 | "tb1qltz8j6hlqk4yhy9svrvtkpxwtr8r7u3a6p3h807vw2l9ukenh3zql3lyr5": [], 19 | "tb1qmsjj65hulcq7md2tuehzmjqflfvnwj7l07mma742qz4f4wan8g2s4amhl9": [], 20 | "tb1qp9tra3arskg62gu5nxw3jmyvs6ztrrcewv83ffrzmr7zl44g9h5q0p3zty": [], 21 | "tb1qqpxtasu8qgfp06y4nkzsp00c5zzals7dz8htje8mndyehngwklaqqy3anx": [], 22 | "tb1qr3t2vus26698u5yujzddlg9v3x8k7edxr7trehaujlfe7r5u9hcqpdg55f": [], 23 | "tb1qrscwrueyf88vpcu2uud5etn6vfew06xavj52ctctg093du700vdsl2hxdt": [], 24 | "tb1qsnrnadkwdgxs2twgx574rk9wz55an8stv47kykpy7vmjt8nzgwssw423mw": [], 25 | "tb1qt5304r3wpmn3zpfs4r7y5x6hakyk2u7dz82wwkns07jhn03lmyqq5cy2z2": [], 26 | "tb1qtjd7qljs99tx8736j7u5h63gychx8l50e9y9ckmpmtngnuv7njcsmd6pux": [], 27 | "tb1qtva4s7v36e8sjz6ppartjmq7zjqekmn44va6t7zmdu5vnhvxka0snpduk7": [], 28 | "tb1qumxldtf5q2wzq95ask46md04ta6j57yw99x7fs5ykvtazqr8pg4q6z83py": [], 29 | "tb1qvl3khp2drn54g7tnmrm94qlanjs3a3fjvccxvmqchw5km7rrt48q6w3942": [], 30 | "tb1qwvcpet99frsw8v6rkuymw969uwrc9vs5dpy04ru8f45xl8s6u3kq96tt6m": [], 31 | "tb1qxknn8rzr924djmqycc0056vnkr7ktvkpm4j9edvyfdp5afuskdtq5m9e3f": [], 32 | "tb1qyq665pgerswlqphtfdeh6h9eda2y2qnep3gtzm6u0zfgvlmvnc3sk0nurd": [] 33 | }, 34 | "addresses": { 35 | "change": [ 36 | "tb1qwvcpet99frsw8v6rkuymw969uwrc9vs5dpy04ru8f45xl8s6u3kq96tt6m", 37 | "tb1qg2cdtu7c860qgm894l65m4drzpvhskmvsx8cr6h7c72gmmeys6aqvj8cya", 38 | "tb1q6cdpn4d373nnkvn682s37dsyhvy3y6lksdqpuzr9k3ch72mxmhzsa8ed32", 39 | "tb1qcydf4ax86d8z7qf4tktta08c6ulwccuptwtg2gqt22cyq07j40rsenp7zs", 40 | "tb1qtjd7qljs99tx8736j7u5h63gychx8l50e9y9ckmpmtngnuv7njcsmd6pux", 41 | "tb1qxknn8rzr924djmqycc0056vnkr7ktvkpm4j9edvyfdp5afuskdtq5m9e3f", 42 | "tb1qtva4s7v36e8sjz6ppartjmq7zjqekmn44va6t7zmdu5vnhvxka0snpduk7", 43 | "tb1qfr3gvv4p67l5zahnppqzjv3fd30c6a5gf7u74r7hyv6snsz6usvszyq40n", 44 | "tb1qk2cferyakf9rey9gpjmgt233ryk3v6tca26ywqmzmyarahha8s9q6yveda", 45 | "tb1qdm29t5pe2ar86d3vz956u8nme425ftq9hwtax7nrngg7ukhfnqjqsn8wnk" 46 | ], 47 | "receiving": [ 48 | "tb1q4n590s3wlqtsz4g6jzmfc8whn0fvun3vmjzpuj5740gr56jfjy2qpglrkv", 49 | "tb1qltz8j6hlqk4yhy9svrvtkpxwtr8r7u3a6p3h807vw2l9ukenh3zql3lyr5", 50 | "tb1qets9smu25enexn4r66el43e473jgv42vmtwm52mc3xc9knp2wmes48skss", 51 | "tb1q8av3me42vpffyad9k98z69mpv2k9yw85p8qj0az9mk8scw2mu05sldqaak", 52 | "tb1qr3t2vus26698u5yujzddlg9v3x8k7edxr7trehaujlfe7r5u9hcqpdg55f", 53 | "tb1qsnrnadkwdgxs2twgx574rk9wz55an8stv47kykpy7vmjt8nzgwssw423mw", 54 | "tb1qumxldtf5q2wzq95ask46md04ta6j57yw99x7fs5ykvtazqr8pg4q6z83py", 55 | "tb1qrscwrueyf88vpcu2uud5etn6vfew06xavj52ctctg093du700vdsl2hxdt", 56 | "tb1qej6rrdlthzv445r2sar4dku8t6wm0294wwu75s264qes40w3d89qj2gn7q", 57 | "tb1qejz6wfrq909mgqe2qla0exdux544xvy6afrntl24wqe349t3jw4s3fxv79", 58 | "tb1qt5304r3wpmn3zpfs4r7y5x6hakyk2u7dz82wwkns07jhn03lmyqq5cy2z2", 59 | "tb1qvl3khp2drn54g7tnmrm94qlanjs3a3fjvccxvmqchw5km7rrt48q6w3942", 60 | "tb1qgpgwsd47hud3l288283l3ndg37kqfgas9t6eeh6h6a7kaek3lzsswfk9le", 61 | "tb1qmsjj65hulcq7md2tuehzmjqflfvnwj7l07mma742qz4f4wan8g2s4amhl9", 62 | "tb1qqpxtasu8qgfp06y4nkzsp00c5zzals7dz8htje8mndyehngwklaqqy3anx", 63 | "tb1qc7m5fnvnwt9xyng9grcwq6yjmegcxzcxc7hud08cukdlmc43mz3qq56f03", 64 | "tb1qp9tra3arskg62gu5nxw3jmyvs6ztrrcewv83ffrzmr7zl44g9h5q0p3zty", 65 | "tb1qd9nlskrzekuc582p5e7qkd7aghaf4gqku602pn2c5lez2krdf6qsrtay0x", 66 | "tb1q4dhcva207vq225ljqfht0xr3ydfmcjj9mcnn9rqc322fpndehhqq9d596z", 67 | "tb1qyq665pgerswlqphtfdeh6h9eda2y2qnep3gtzm6u0zfgvlmvnc3sk0nurd" 68 | ] 69 | }, 70 | "channel_backups": {}, 71 | "fiat_value": {}, 72 | "invoices": {}, 73 | "labels": {}, 74 | "payment_requests": {}, 75 | "prevouts_by_scripthash": {}, 76 | "seed_version": 33, 77 | "spent_outpoints": {}, 78 | "transactions": {}, 79 | "tx_fees": {}, 80 | "txi": {}, 81 | "txo": {}, 82 | "use_encryption": false, 83 | "verified_tx3": {}, 84 | "wallet_type": "2of2", 85 | "x1/": { 86 | "derivation": "m/48'/1'/0'/2'", 87 | "hw_type": "bitbox02", 88 | "label": "bb2 (27d81095)", 89 | "root_fingerprint": "27d81095", 90 | "soft_device_id": "27d81095", 91 | "type": "hardware", 92 | "xpub": "Vpub5mUs4UNPA6T3VAmcTWTJ2nCV2oAEFQqBNQQDH62NQNpdAMSyL2Nd3vZXF6uQfNeiCst7asUapZWM9AKmsYCK1BjUrEVhiVm9M4qnbHvDRDe" 93 | }, 94 | "x2/": { 95 | "derivation": "m/48'/100'/0'/2'", 96 | "hw_type": "trezor", 97 | "label": "T-Rex", 98 | "root_fingerprint": "6bfac2d6", 99 | "soft_device_id": "EE735C3D13E9A60902B42A02", 100 | "type": "hardware", 101 | "xpub": "Vpub5mTgvNLEMssnVd4fezZgnDhLefVaYCb94GsjHfgrhXksbrHRbMa2AwjWX9eczB1dG19oZmEnVNCeVLWoygXQrkL8nuyAgWDxnePWUQ5fE3N" 102 | } 103 | } -------------------------------------------------------------------------------- /tests/wallets/multisig_legacy: -------------------------------------------------------------------------------- 1 | { 2 | "addr_history": { 3 | "2MumcbuHasUggZojuujUPeEmBGDLf4iptai": [], 4 | "2MuphqNK6X9Rb1NcotjRRk26uDjmtxY19Jn": [], 5 | "2Mw8uv2b3ZfMobPdFE3XkMzCwEydyGzjBG8": [], 6 | "2MwUuzxcvrWWhSLp5TMFD1Vx9pSvSybK2BK": [], 7 | "2MxBoT1VyGErp3z3GpaaKMByT133jCCacLJ": [], 8 | "2Mxh5M1t3GoZvYuxggJsy218g5kkU4Axvuw": [], 9 | "2MxzkfVbQQhzvqFET5id3Cxk8L6LM76ysSa": [], 10 | "2MyniMDDzoh4qi3MHz4SVHtex2n3uZJpTMu": [], 11 | "2MysXPKYfuVMbDokitbKocr2yyyyZD1qqoB": [], 12 | "2MyuZzTkAxL8gViGb9pMwhW1vasrfQdMQZH": [], 13 | "2N1FdPRJgrMJeiLex2skGW3CcNcXj2VDdq7": [], 14 | "2N1tCD57jYPD3zdpbR3TWL8r5BSBGNqn8mv": [], 15 | "2N1uUejUtqxCCPFqVCrdqN4S6uqoWKAFomQ": [], 16 | "2N3KhGCSqEaVYkJmKdi22RPwQt7RbWGnE4J": [], 17 | "2N3SQ21TAapPXLCJZystXM57EXhppuuFYdH": [], 18 | "2N4TfccireixfPhbqZKp8SqX1K9Q3FUd1mq": [], 19 | "2N4bCR9ZNnGbcoptZ88VZjqiwfZy6tNWw9r": [], 20 | "2N57xfxs1wYM2w5VDwKMw3WdGTdiG92HV4A": [], 21 | "2N5BvJBNA2Hi1x5YNNqzh37eY1ofTCgDey7": [], 22 | "2N6s8xeNMgvokwgeEwWdKKfSdVYLpdPGGQZ": [], 23 | "2N6wrZHiaV4SVQ9edwpvr26zdVtiLTUaxED": [], 24 | "2N7RiHeaiJ3ao8Hic4GmuKrobE6FJHPRnRf": [], 25 | "2N8EPE8Jk9PjpQFnGZvgkS3BeXpkpaaeFtJ": [], 26 | "2N8wreaWKYzxcn1Num5WG5D1bDyiFfXw4D4": [], 27 | "2NDXGFBLJARpoDZtiu55uihoScBdwWaa4Rq": [], 28 | "2NDuHLJfjQWSdaEZDDMVDMu7ehZJRLcBXYn": [], 29 | "2NEi9cvB1TE1xEfa3P2B886fuPbTfJC7rkW": [], 30 | "2NFc7juiUa9AWbCjNWe4AmR7b28oA4gxT8H": [], 31 | "2NFniXtUFfe6ks9dbfCgxNd588oMKXepsEK": [], 32 | "2NG3NzvxAZAbMZaSoRhD4dabxHmEMFwFsaM": [] 33 | }, 34 | "addresses": { 35 | "change": [ 36 | "2N57xfxs1wYM2w5VDwKMw3WdGTdiG92HV4A", 37 | "2N3KhGCSqEaVYkJmKdi22RPwQt7RbWGnE4J", 38 | "2NDXGFBLJARpoDZtiu55uihoScBdwWaa4Rq", 39 | "2Mxh5M1t3GoZvYuxggJsy218g5kkU4Axvuw", 40 | "2NEi9cvB1TE1xEfa3P2B886fuPbTfJC7rkW", 41 | "2N5BvJBNA2Hi1x5YNNqzh37eY1ofTCgDey7", 42 | "2MysXPKYfuVMbDokitbKocr2yyyyZD1qqoB", 43 | "2MumcbuHasUggZojuujUPeEmBGDLf4iptai", 44 | "2N8wreaWKYzxcn1Num5WG5D1bDyiFfXw4D4", 45 | "2N1tCD57jYPD3zdpbR3TWL8r5BSBGNqn8mv" 46 | ], 47 | "receiving": [ 48 | "2N7RiHeaiJ3ao8Hic4GmuKrobE6FJHPRnRf", 49 | "2N4TfccireixfPhbqZKp8SqX1K9Q3FUd1mq", 50 | "2MuphqNK6X9Rb1NcotjRRk26uDjmtxY19Jn", 51 | "2NFc7juiUa9AWbCjNWe4AmR7b28oA4gxT8H", 52 | "2N3SQ21TAapPXLCJZystXM57EXhppuuFYdH", 53 | "2NG3NzvxAZAbMZaSoRhD4dabxHmEMFwFsaM", 54 | "2N1uUejUtqxCCPFqVCrdqN4S6uqoWKAFomQ", 55 | "2MwUuzxcvrWWhSLp5TMFD1Vx9pSvSybK2BK", 56 | "2N4bCR9ZNnGbcoptZ88VZjqiwfZy6tNWw9r", 57 | "2N8EPE8Jk9PjpQFnGZvgkS3BeXpkpaaeFtJ", 58 | "2NFniXtUFfe6ks9dbfCgxNd588oMKXepsEK", 59 | "2MyuZzTkAxL8gViGb9pMwhW1vasrfQdMQZH", 60 | "2MxBoT1VyGErp3z3GpaaKMByT133jCCacLJ", 61 | "2Mw8uv2b3ZfMobPdFE3XkMzCwEydyGzjBG8", 62 | "2MxzkfVbQQhzvqFET5id3Cxk8L6LM76ysSa", 63 | "2MyniMDDzoh4qi3MHz4SVHtex2n3uZJpTMu", 64 | "2N1FdPRJgrMJeiLex2skGW3CcNcXj2VDdq7", 65 | "2N6wrZHiaV4SVQ9edwpvr26zdVtiLTUaxED", 66 | "2N6s8xeNMgvokwgeEwWdKKfSdVYLpdPGGQZ", 67 | "2NDuHLJfjQWSdaEZDDMVDMu7ehZJRLcBXYn" 68 | ] 69 | }, 70 | "channel_backups": {}, 71 | "fiat_value": {}, 72 | "invoices": {}, 73 | "labels": {}, 74 | "payment_requests": {}, 75 | "prevouts_by_scripthash": {}, 76 | "qt-console-history": [], 77 | "seed_version": 33, 78 | "spent_outpoints": {}, 79 | "stored_height": 2134485, 80 | "transactions": {}, 81 | "tx_fees": {}, 82 | "txi": {}, 83 | "txo": {}, 84 | "use_encryption": false, 85 | "verified_tx3": {}, 86 | "wallet_type": "2of2", 87 | "winpos-qt": [ 88 | 2085, 89 | 222, 90 | 840, 91 | 468 92 | ], 93 | "x1/": { 94 | "derivation": "m", 95 | "pw_hash_version": 1, 96 | "root_fingerprint": "a0a60157", 97 | "seed": "marble duck village valley elite convince slogan all bulk window deer crash", 98 | "type": "bip32", 99 | "xprv": "tprv8ZgxMBicQKsPeLPWr5WbJDAhANr6irc1Yf7eUNCYjGYap27HU4bDBXWGMT3X75FhDyxNXr6pK4QeHcCBvkqchQzK8wZ4JbGv5X5MWtXQtqy", 100 | "xpub": "tpubD6NzVbkrYhZ4XoRJjjBBhcpojQN2tBnv7xiRktEr9YLyeWN46TQoN288XaNHXPqsFJQTmkifbr5MpDknrDpwnCyiPP2qWZu1gWRdxKgXCyE" 101 | }, 102 | "x2/": { 103 | "derivation": "m", 104 | "pw_hash_version": 1, 105 | "root_fingerprint": "230b70d2", 106 | "type": "bip32", 107 | "xprv": null, 108 | "xpub": "tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt" 109 | } 110 | } -------------------------------------------------------------------------------- /tests/wallets/multisig_segwit: -------------------------------------------------------------------------------- 1 | { 2 | "addr_history": { 3 | "tb1q0v3yu6rxuvd90ur0g6hx69dhlemjdqz7fgzly8hzhwpp7kntg0dsfa0964": [], 4 | "tb1q36kysjh3mv6sgc5amzmysn3neqr230f0qjrfx8ersfkqhcf6z33ss04lp9": [], 5 | "tb1q53qxdzdxmnkuckg9lv7mk8djgq2nqkjgku5vlmg73y8an80k4f5qn9hr05": [], 6 | "tb1q5x0zdlz83wra76p6fx5t9gnwekujngflyxq6pad3q6zj9yl0lkfspzvaq3": [], 7 | "tb1q6x5sfksrqg20668q0vj3phfjkrqkzx62ke3mutflxhsca4gjc48qgeusmx": [], 8 | "tb1q7p5wy59qauzrwq2n5rddqq3l7wqs7lly9553j3et5slu0kx4589sd79q8r": [], 9 | "tb1q7s26yhh8hmq2t6r8wa7ckpjhcugkhjshnw5hsg0m9u82kmmy5zrqdm2qc2": [], 10 | "tb1q885t7wwtky4djyahs4vscqdgelq4z9yfky9es5fexehh86pzdhws2hhz0x": [], 11 | "tb1q8mrzmsay59eln9pdvfvae7mevwfxme7v86s9y7q5vgqx5aajwrzsjylq6v": [], 12 | "tb1q8wlr5lycw2ku9djq45dpgezem4hr6l4msd667e50g7tlryq7mwsqnesayz": [], 13 | "tb1q9a4yc2vwep2xefphhedevxz4uj0x5mjj0y0p72cta8eytlghg0lsk9ajf8": [], 14 | "tb1qazjtd5kxjztpqslt25y3h2nz3pm49yh8jjax6teav4ct7ptjtypssex05e": [], 15 | "tb1qc85pkc4apkdyyqffxk0wdeuze7wfqd7qcp0fe4c8rc72e73qexgsh0rex2": [], 16 | "tb1qdrgd72r8x89yesxmjs0kkyd8cx3te6h49jna678s7frcf6tuf4fqs05p89": [], 17 | "tb1qewjj5nzrfj9fc73k392sr6pgeeyfvfy3vrxtczr4pyjwpzhez7eqtypafe": [], 18 | "tb1qffqql05583mzcjfwalkusvcnm8avs7h6ths44xg73kv2xkcc0nysca69t0": [], 19 | "tb1ql0h4d90l8wxfuccv008cl37458e8alfrn35x53rgkqzzg0f672fqms35mw": [], 20 | "tb1ql3tm2uts7rcc2xqlzxua3evasdsv2277cs2c92m2f4h5hujrpv6qptx8ex": [], 21 | "tb1qltttpv04grw8e255w8ksuypn5ak45xvn8zt45xwtmj6ttymyx47sdc3h6q": [], 22 | "tb1qna8sx3r037vv24yplumshj8qs6c4aps3pqltr0v6c0p2mhwgmmysm5fz2z": [], 23 | "tb1qpm3sz00wxzqs74faajv5fa8efvrte60jm7gntt5ujeafermhmwxqmea9g7": [], 24 | "tb1qqux89kst6laa6mctn42dp57t68gm5skkdh83xye33g95u86mktus6da05r": [], 25 | "tb1qt2qyqamedfqvjgg83k4fx4njqc387xqtpcwlj9e3hnsgdahp2tcqmvf75z": [], 26 | "tb1qumfvvsjy6yfg83czyxspy6xtmu52g2hv3cqrnu4nfmef4ttstukqawz8x2": [], 27 | "tb1qver6atmpu2q8nrfr53ywtxlnp3va9w2r3fqw5lha6n4t00hwk7wqwwyphd": [], 28 | "tb1qvglpyw38hrnjhnj4v6paa4cj2sfgkxjagvwzw0w3e97f2t47l6aq4kp6nv": [], 29 | "tb1qvz8chfqd3nee96s4fr6neaqk4gz93jdzgccxepqt57xm64gtfras5x420d": [], 30 | "tb1qws4l20wwldrd4ghgm72d4z73fvw87xadte3xyhnk3gzeq56vp7jsrcjmyk": [], 31 | "tb1qxna4v45xnf0j76z7cuetlpx3qdzuk2333f3anm9qymmj6u6w9drq4zzqm2": [], 32 | "tb1qywdkvysahamjzl9pa0wlssmpplyhm2cqasqznhk7pz4x58sh3x7s7yfkea": [] 33 | }, 34 | "addresses": { 35 | "change": [ 36 | "tb1q7s26yhh8hmq2t6r8wa7ckpjhcugkhjshnw5hsg0m9u82kmmy5zrqdm2qc2", 37 | "tb1qewjj5nzrfj9fc73k392sr6pgeeyfvfy3vrxtczr4pyjwpzhez7eqtypafe", 38 | "tb1q36kysjh3mv6sgc5amzmysn3neqr230f0qjrfx8ersfkqhcf6z33ss04lp9", 39 | "tb1q53qxdzdxmnkuckg9lv7mk8djgq2nqkjgku5vlmg73y8an80k4f5qn9hr05", 40 | "tb1q5x0zdlz83wra76p6fx5t9gnwekujngflyxq6pad3q6zj9yl0lkfspzvaq3", 41 | "tb1qc85pkc4apkdyyqffxk0wdeuze7wfqd7qcp0fe4c8rc72e73qexgsh0rex2", 42 | "tb1qffqql05583mzcjfwalkusvcnm8avs7h6ths44xg73kv2xkcc0nysca69t0", 43 | "tb1ql3tm2uts7rcc2xqlzxua3evasdsv2277cs2c92m2f4h5hujrpv6qptx8ex", 44 | "tb1q8wlr5lycw2ku9djq45dpgezem4hr6l4msd667e50g7tlryq7mwsqnesayz", 45 | "tb1qdrgd72r8x89yesxmjs0kkyd8cx3te6h49jna678s7frcf6tuf4fqs05p89" 46 | ], 47 | "receiving": [ 48 | "tb1q6x5sfksrqg20668q0vj3phfjkrqkzx62ke3mutflxhsca4gjc48qgeusmx", 49 | "tb1qxna4v45xnf0j76z7cuetlpx3qdzuk2333f3anm9qymmj6u6w9drq4zzqm2", 50 | "tb1ql0h4d90l8wxfuccv008cl37458e8alfrn35x53rgkqzzg0f672fqms35mw", 51 | "tb1qver6atmpu2q8nrfr53ywtxlnp3va9w2r3fqw5lha6n4t00hwk7wqwwyphd", 52 | "tb1qywdkvysahamjzl9pa0wlssmpplyhm2cqasqznhk7pz4x58sh3x7s7yfkea", 53 | "tb1qna8sx3r037vv24yplumshj8qs6c4aps3pqltr0v6c0p2mhwgmmysm5fz2z", 54 | "tb1qltttpv04grw8e255w8ksuypn5ak45xvn8zt45xwtmj6ttymyx47sdc3h6q", 55 | "tb1q8mrzmsay59eln9pdvfvae7mevwfxme7v86s9y7q5vgqx5aajwrzsjylq6v", 56 | "tb1qazjtd5kxjztpqslt25y3h2nz3pm49yh8jjax6teav4ct7ptjtypssex05e", 57 | "tb1q885t7wwtky4djyahs4vscqdgelq4z9yfky9es5fexehh86pzdhws2hhz0x", 58 | "tb1q9a4yc2vwep2xefphhedevxz4uj0x5mjj0y0p72cta8eytlghg0lsk9ajf8", 59 | "tb1qumfvvsjy6yfg83czyxspy6xtmu52g2hv3cqrnu4nfmef4ttstukqawz8x2", 60 | "tb1qt2qyqamedfqvjgg83k4fx4njqc387xqtpcwlj9e3hnsgdahp2tcqmvf75z", 61 | "tb1qpm3sz00wxzqs74faajv5fa8efvrte60jm7gntt5ujeafermhmwxqmea9g7", 62 | "tb1qws4l20wwldrd4ghgm72d4z73fvw87xadte3xyhnk3gzeq56vp7jsrcjmyk", 63 | "tb1qqux89kst6laa6mctn42dp57t68gm5skkdh83xye33g95u86mktus6da05r", 64 | "tb1qvglpyw38hrnjhnj4v6paa4cj2sfgkxjagvwzw0w3e97f2t47l6aq4kp6nv", 65 | "tb1q7p5wy59qauzrwq2n5rddqq3l7wqs7lly9553j3et5slu0kx4589sd79q8r", 66 | "tb1qvz8chfqd3nee96s4fr6neaqk4gz93jdzgccxepqt57xm64gtfras5x420d", 67 | "tb1q0v3yu6rxuvd90ur0g6hx69dhlemjdqz7fgzly8hzhwpp7kntg0dsfa0964" 68 | ] 69 | }, 70 | "channel_backups": {}, 71 | "fiat_value": {}, 72 | "invoices": {}, 73 | "labels": {}, 74 | "payment_requests": {}, 75 | "prevouts_by_scripthash": {}, 76 | "seed_version": 33, 77 | "spent_outpoints": {}, 78 | "transactions": {}, 79 | "tx_fees": {}, 80 | "txi": {}, 81 | "txo": {}, 82 | "use_encryption": false, 83 | "verified_tx3": {}, 84 | "wallet_type": "2of2", 85 | "x1/": { 86 | "derivation": "m/1'", 87 | "pw_hash_version": 1, 88 | "root_fingerprint": "f6083804", 89 | "seed": "maximum assume mention girl puppy stereo river tourist gossip level panda life", 90 | "type": "bip32", 91 | "xprv": "Vprv1AEubNnoCJHpVL2TLDscvM6bEC7Q4ZxqkrdhxCHFb6EJtADGoSkb9cXt9tTFYqgEVSJGhM96eZmwK1G88SW2Zg1LayXACst8XFYSgS8dK8b", 92 | "xpub": "Vpub5gvwjnq3LfyVNo4FDsYDKkkhoDdLDu9kLAEVEiKZ1N2hieU3RqaBL79kL2wP5YNj2aL9Bbe5bFXm2BFbpStm5ixxo8SKQrCBK5DxSrJhq7k" 93 | }, 94 | "x2/": { 95 | "derivation": "m/1'", 96 | "pw_hash_version": 1, 97 | "root_fingerprint": "b88448fb", 98 | "type": "bip32", 99 | "xprv": null, 100 | "xpub": "Vpub5gUii5ZKgrJs2sZfyHrKFrKdZqmek7iLGoX3jmREfdMscpyaDPLAcYm1mED7PexvD4JQPMJL3AGtMewxSjFhcUX64fKFR2bgLXAY7xPgJaX" 101 | } 102 | } -------------------------------------------------------------------------------- /tests/wallets/multisig_wrapped_watch: -------------------------------------------------------------------------------- 1 | { 2 | "addr_history": { }, 3 | "addresses": { 4 | "change": [ 5 | "2Mz7yo9W6wyBLfiKCXwZeg2sio1UMj9j624", 6 | "2Mx9kNoesax7NsNXNF68Udgmcf8yVbp81pz", 7 | "2ND6DmPJQQ21iwuVYpG3tCGrXXKrvK73dUB", 8 | "2Mwkqy6Lt62dLPyTuBMdHJR4gC1rwV5F5QK", 9 | "2NBDdgYcE3sJAyYggVtTSSeq2Vm52ZBtARa", 10 | "2N69Nht2Rtcen6VpGFLEV1v9Q4emsD2G2zd", 11 | "2N9rdfLRVpvmpLzFgLjSHk1CFbnzkhcxtWb", 12 | "2MxcziECWdpHoZyx6mjRR179HEEso142wcq", 13 | "2MwZHeUHp4WhAfQ7TtwEuRY9qjpwVTFYGZ5", 14 | "2N1zStXEUmdN5E2F9zjCgcLko4Tf6f4HBfy", 15 | "2NCe3jpXxAkaozerBXQWPV79ZqsUHTML751", 16 | "2MxoJ5Hi7GbTtaXwF9xPmttjvgGcPWP2cAG", 17 | "2NFddKeHiBf6DBh1JiPTnuwTnYRZfdyahpJ", 18 | "2NAdFW3oSUE9XaGymwXhwuPXQtaEyP8Bfvi", 19 | "2NCESXSYtnYsqgH6VcuQRBirwvcaWqeg1yC", 20 | "2NCitx7KukLjTfdrjCoiaD1EwZ7ecj5tvbz", 21 | "2NEPVvYoEdvHygZWskqVeMBT436LiTwLLSa", 22 | "2N9zrbQUBW861aiJ3RZnwyEX8YwkgRMvhWd", 23 | "2NC2vDp9pDATrEuMij7u2Hr7NZxVhgrzr2V", 24 | "2NCXGC3HQhc8Dp6FSxnAVGwSuRHVv5HE3AB", 25 | "2Mvvx8GUBMuXTbjFpnr61TiVnG8t9sR8znk", 26 | "2MyumsSnmeVWmye1MqxPGXq2mAdtXz8TAaB" 27 | ], 28 | "receiving": [ 29 | "2Mtkk3kjyN8hgdGXPuJCNnwS3BBY4K2frhY", 30 | "2NEua1ZffPXrPuDnKJv8SaepBi6d9VR5Shu", 31 | "2N7R9gs3ELT6ZvTQRg8atcf3qW6EYREBLty", 32 | "2NBknD44Y8rsnZBBU33UMDLjn5pdMrs6TEP", 33 | "2NEPDS2RsiisUcUeghiRkFaRSe3z86VXojz", 34 | "2NGUTARfPZSySMp9e5CDTamk2SFSUHTxjp9", 35 | "2NFeYgjnFNUAQ1G1MTaaH327jjWzhYNowzM", 36 | "2N8DGRzUjnZKKWLx2oC1wVyu9s2ARqaSYJS", 37 | "2N5CvKw2D2SCJkQvSyotuNnAsr3kZy3Ydxy", 38 | "2MycZTRACxt14w5KSBxWoCZQVMmuguEtr3f", 39 | "2N3tKoojjSVEepmarTGo6Yim9UeESoiDZBG", 40 | "2N4ssm6Vf2mkWpqT6k9uCtqHt8M3iAS1Yme", 41 | "2NBXY7x81nsvTySkfURUA1YyoiAJv1uP9AD", 42 | "2N2g397tjB2Uq6gGba4jCoJ7pZ9WexxGABE", 43 | "2MuES2UKKzB1wuooRSJBfzo81N4eQd6AnTm", 44 | "2MueQ2MrfrepxFH1ByxtqUSpmWDzrjTe2HQ", 45 | "2MxowtnedZ2eb2Z9ytaQK6rQKt95vyZS5kV", 46 | "2NDSmLunv1UWYVhABphGFFgePKAsxrL5b7n", 47 | "2NF8PfgiXrpnxDmVxxsJMiQehscNZCfCtaf", 48 | "2N7WnuQg4MXW6Nbe7a8uKk5b4xKHqoMeZrV", 49 | "2NDQTYzLgUn1ayHsLsFHbmG58SkfoBse3pe" 50 | ] 51 | }, 52 | "channel_backups": {}, 53 | "channels": {}, 54 | "fiat_value": {}, 55 | "invoices": {}, 56 | "labels": {}, 57 | "payment_requests": {}, 58 | "prevouts_by_scripthash": {}, 59 | "qt-console-history": [], 60 | "seed_version": 33, 61 | "spent_outpoints": {}, 62 | "stored_height": 2134584, 63 | "transactions": {}, 64 | "tx_fees": {}, 65 | "txi": {}, 66 | "txo": {}, 67 | "use_change": true, 68 | "use_encryption": false, 69 | "verified_tx3": {}, 70 | "wallet_type": "3of7", 71 | "winpos-qt": [ 72 | 2015, 73 | 351, 74 | 840, 75 | 468 76 | ], 77 | "x1/": { 78 | "derivation": null, 79 | "pw_hash_version": 1, 80 | "root_fingerprint": null, 81 | "type": "bip32", 82 | "xprv": null, 83 | "xpub": "Upub5SuW82zTTrULvC2KTgFqADind2gonSSvB345ALrdtXx8pN94auhnxditBo7HLUPJV5SAfaofHhgBaet5eXgaSxmpVoVeSmhWorgEEL1ipzY" 84 | }, 85 | "x2/": { 86 | "derivation": null, 87 | "pw_hash_version": 1, 88 | "root_fingerprint": null, 89 | "type": "bip32", 90 | "xprv": null, 91 | "xpub": "Upub5T5HWwZsXJLQ8RJyPzviXn4fF7bv73kV3Rc3jRJ5mU1iphzrcJQ4kZ98jWx1PebwDPrNhBF5sAWHw9qVEt5HSFHmiNxG5PmCbQif9Lk4q8Z" 92 | }, 93 | "x3/": { 94 | "derivation": null, 95 | "pw_hash_version": 1, 96 | "root_fingerprint": null, 97 | "type": "bip32", 98 | "xprv": null, 99 | "xpub": "Upub5S371fWWWjS4d3ZcuUiRWFdNVJsS2v3NK79g9XacG4UpV59v9PdhHXcr9gKCNvGG59bHMo1oM83hZefgBCidGizUPhcZ3BHUDtBttrWwm7r" 100 | }, 101 | "x4/": { 102 | "derivation": null, 103 | "pw_hash_version": 1, 104 | "root_fingerprint": null, 105 | "type": "bip32", 106 | "xprv": null, 107 | "xpub": "Upub5TNGnoRwiQgEe8fSyvsgJGBcu6EACeFYj5G89RhUFYA1GCS2iHofv6yHTafh4sCFGTFx6tW2naMAcYivQdQZEqffzeVQCheKFL7aH1V2paZ" 108 | }, 109 | "x5/": { 110 | "derivation": null, 111 | "pw_hash_version": 1, 112 | "root_fingerprint": null, 113 | "type": "bip32", 114 | "xprv": null, 115 | "xpub": "Upub5SXsqN5ev23dZRAopqVu4PGZmDnCzwBR9ovrJYiejfMX5AE35bxM5YrP2gZP7nRqdezy7D6TXHABrdhWtKm4rzPMMn2VJUuGH5Dgyq6KNGP" 116 | }, 117 | "x6/": { 118 | "derivation": null, 119 | "pw_hash_version": 1, 120 | "root_fingerprint": null, 121 | "type": "bip32", 122 | "xprv": null, 123 | "xpub": "Upub5SKeFkex2kf375rrQJfR95nhr1XxkLz1KAfuRHscm7ngZ5RAPFjVtBTmQHVQTxy3gXxJfQ1FiZyafms4nQKcSBLRu26pLfRT22msHAKbfcJ" 124 | }, 125 | "x7/": { 126 | "derivation": null, 127 | "pw_hash_version": 1, 128 | "root_fingerprint": null, 129 | "type": "bip32", 130 | "xprv": null, 131 | "xpub": "Upub5Ss1Vu6fAakq5PmE7VsyGsvywbzx467sKHTCCLtycFpx9Nw5JWgYCafztN95i3jPEZcQaKEji225EMVStQQsjxxBxbcv5cSd88vXA1ruU9o" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/wallets2descriptors.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "wallet_file")] 2 | use bdk_wallet::{bitcoin::Network, KeychainKind, Wallet}; 3 | use libelectrum2descriptors::{Descriptors, ElectrumWalletFile}; 4 | use rstest::rstest; 5 | use std::{ 6 | path::{Path, PathBuf}, 7 | str::FromStr, 8 | }; 9 | use tempfile::tempdir; 10 | 11 | #[rstest] 12 | #[case::default_legacy("default_legacy", 13 | "pkh(tprv8ZgxMBicQKsPeYnCHtn5QZqhTgkkDmXebfQMXWmX7ThXJFCbzDTKFNRsB43GUmHzu2pdGcnnegFy175kFcgZQYC5BFPnRdYDPQyqetpyjb5/0/*)", 14 | "pkh(tprv8ZgxMBicQKsPeYnCHtn5QZqhTgkkDmXebfQMXWmX7ThXJFCbzDTKFNRsB43GUmHzu2pdGcnnegFy175kFcgZQYC5BFPnRdYDPQyqetpyjb5/1/*)")] 15 | #[case::default_legacy_watch("default_legacy_watch", 16 | "pkh(tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt/0/*)", 17 | "pkh(tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt/1/*)")] 18 | #[case::default_segwit("default_segwit", 19 | "wpkh(tprv8cvkZzx9zA7EfFDbH945mK23r7hg6EHXUk79wVUSRukwyctFS1AdpSpkZcykAMDveCj8RA3R4jwFTKMwMbWexJox8NMqq7YphJLDumfCSfu/0/*)", 20 | "wpkh(tprv8cvkZzx9zA7EfFDbH945mK23r7hg6EHXUk79wVUSRukwyctFS1AdpSpkZcykAMDveCj8RA3R4jwFTKMwMbWexJox8NMqq7YphJLDumfCSfu/1/*)")] 21 | #[case::multisig_hw_segwit("multisig_hw_segwit", 22 | "wsh(sortedmulti(2,tpubDEcw4ooTbmw62zBKdkYepoP3z4WWugdeRzPHHAbk8XVsPfBE9AAZMNghiqwtdFgtabaeppBTPmezUkRkQZidLcSJp3XTASbMakHcYauWehZ/0/*,tpubDEbkvhmJoZMq3SUNqEf3aEsubvqsCUPc7rroHkGERgS7qA1gQVMxUPrgzth6x43odirLohwf4aMHpvcnWi3jCB2xkizv8T4B2KqLRZVLC6K/0/*))", 23 | "wsh(sortedmulti(2,tpubDEcw4ooTbmw62zBKdkYepoP3z4WWugdeRzPHHAbk8XVsPfBE9AAZMNghiqwtdFgtabaeppBTPmezUkRkQZidLcSJp3XTASbMakHcYauWehZ/1/*,tpubDEbkvhmJoZMq3SUNqEf3aEsubvqsCUPc7rroHkGERgS7qA1gQVMxUPrgzth6x43odirLohwf4aMHpvcnWi3jCB2xkizv8T4B2KqLRZVLC6K/1/*))")] 24 | #[case::multisig_legacy("multisig_legacy", 25 | "sh(sortedmulti(2,tprv8ZgxMBicQKsPeLPWr5WbJDAhANr6irc1Yf7eUNCYjGYap27HU4bDBXWGMT3X75FhDyxNXr6pK4QeHcCBvkqchQzK8wZ4JbGv5X5MWtXQtqy/0/*,tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt/0/*))", 26 | "sh(sortedmulti(2,tprv8ZgxMBicQKsPeLPWr5WbJDAhANr6irc1Yf7eUNCYjGYap27HU4bDBXWGMT3X75FhDyxNXr6pK4QeHcCBvkqchQzK8wZ4JbGv5X5MWtXQtqy/1/*,tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt/1/*))")] 27 | #[case::multisig_segwit("multisig_segwit", 28 | "wsh(sortedmulti(2,tprv8dNybiDsdyms39SAWTxyiNHABTTgiqmJpScmxGrdKEuZ7TwXcaYXT4f4ddVjWiiQs9zowHqyDmvaebN6fU2Lu6iAYnYuepiLkvzGdcZZi8D/0/*,tpubD9cniQzQ8XnuagyP9Xwg3sWCX77wQPWoLPW7jqzcPn37r8hq2X86uztCEyFbMY16amzwdJ1CcNRXhF3vykn1wuDv2ULzryRtaCcN5Cr8F9y/0/*))", 29 | "wsh(sortedmulti(2,tprv8dNybiDsdyms39SAWTxyiNHABTTgiqmJpScmxGrdKEuZ7TwXcaYXT4f4ddVjWiiQs9zowHqyDmvaebN6fU2Lu6iAYnYuepiLkvzGdcZZi8D/1/*,tpubD9cniQzQ8XnuagyP9Xwg3sWCX77wQPWoLPW7jqzcPn37r8hq2X86uztCEyFbMY16amzwdJ1CcNRXhF3vykn1wuDv2ULzryRtaCcN5Cr8F9y/1/*))")] 30 | #[case::multisig_wrapped_watch("multisig_wrapped_watch", 31 | "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))", 32 | "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/1/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/1/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/1/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/1/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/1/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/1/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/1/*)))")] 33 | fn parse_wallet( 34 | #[case] wallet_name: &str, 35 | #[case] expected_descriptor_ext: &str, 36 | #[case] expected_descriptor_chg: &str, 37 | ) { 38 | let desc = wallet_name_to_descriptors(wallet_name); 39 | assert_eq!(desc.external, expected_descriptor_ext); 40 | assert_eq!(desc.change, expected_descriptor_chg); 41 | let addr = first_address_from_descriptor(&desc.external, Network::Testnet); 42 | let exp = first_address_from_wallet_file(wallet_name); 43 | assert_eq!(addr, exp); 44 | } 45 | 46 | fn wallet_name_to_descriptors(wallet_name: &str) -> Descriptors { 47 | let wallet_file = get_test_wallet_file(wallet_name); 48 | let wallet = ElectrumWalletFile::from_file(wallet_file.as_path()).unwrap(); 49 | wallet.to_descriptors().unwrap() 50 | } 51 | 52 | fn get_test_wallet_file(wallet_name: &str) -> PathBuf { 53 | let test_dir = Path::new(file!()).canonicalize().unwrap(); 54 | let test_dir = test_dir.as_path().parent().unwrap(); 55 | let wallet_file = test_dir.join("wallets/".to_string() + wallet_name); 56 | assert!( 57 | wallet_file.exists(), 58 | "File not found: {}", 59 | wallet_file.to_str().unwrap() 60 | ); 61 | wallet_file.to_path_buf() 62 | } 63 | 64 | fn first_address_from_descriptor(desc: &str, network: Network) -> String { 65 | let mut wallet = Wallet::create_single(desc.to_string()) 66 | .network(network) 67 | .create_wallet_no_persist() 68 | .unwrap(); 69 | wallet 70 | .reveal_next_address(KeychainKind::External) 71 | .address 72 | .to_string() 73 | } 74 | 75 | fn first_address_from_wallet_file(wallet_name: &str) -> String { 76 | let wallet_file = get_test_wallet_file(wallet_name); 77 | let wallet = ElectrumWalletFile::from_file(wallet_file.as_path()).unwrap(); 78 | wallet.addresses().receiving[0].clone() 79 | } 80 | 81 | /// Since converting a wallet with imported keys or addresses can't be converted to a descriptor anyway, we just leave a not so descriptive error message due to a different json format of such wallets. 82 | #[rstest] 83 | #[case::imported_addr("imported_addr")] 84 | #[case::imported_privkey("imported_privkey")] 85 | #[should_panic(expected = "missing field `change`")] 86 | fn parse_imported(#[case] wallet_name: &str) { 87 | let wallet_file = get_test_wallet_file(wallet_name); 88 | let _wallet = ElectrumWalletFile::from_file(wallet_file.as_path()).unwrap(); 89 | } 90 | 91 | #[rstest] 92 | #[case::default_legacy("default_legacy", 93 | "pkh(tprv8ZgxMBicQKsPeYnCHtn5QZqhTgkkDmXebfQMXWmX7ThXJFCbzDTKFNRsB43GUmHzu2pdGcnnegFy175kFcgZQYC5BFPnRdYDPQyqetpyjb5/0/*)")] 94 | #[case::default_legacy_watch("default_legacy_watch", 95 | "pkh(tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt/0/*)")] 96 | #[case::default_segwit("default_segwit", 97 | "wpkh(tprv8cvkZzx9zA7EfFDbH945mK23r7hg6EHXUk79wVUSRukwyctFS1AdpSpkZcykAMDveCj8RA3R4jwFTKMwMbWexJox8NMqq7YphJLDumfCSfu/0/*)")] 98 | #[case::multisig_hw_segwit("multisig_hw_segwit", 99 | "wsh(sortedmulti(2,tpubDEcw4ooTbmw62zBKdkYepoP3z4WWugdeRzPHHAbk8XVsPfBE9AAZMNghiqwtdFgtabaeppBTPmezUkRkQZidLcSJp3XTASbMakHcYauWehZ/0/*,tpubDEbkvhmJoZMq3SUNqEf3aEsubvqsCUPc7rroHkGERgS7qA1gQVMxUPrgzth6x43odirLohwf4aMHpvcnWi3jCB2xkizv8T4B2KqLRZVLC6K/0/*))")] 100 | #[case::multisig_legacy("multisig_legacy", 101 | "sh(sortedmulti(2,tprv8ZgxMBicQKsPeLPWr5WbJDAhANr6irc1Yf7eUNCYjGYap27HU4bDBXWGMT3X75FhDyxNXr6pK4QeHcCBvkqchQzK8wZ4JbGv5X5MWtXQtqy/0/*,tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt/0/*))")] 102 | #[case::multisig_segwit("multisig_segwit", 103 | "wsh(sortedmulti(2,tprv8dNybiDsdyms39SAWTxyiNHABTTgiqmJpScmxGrdKEuZ7TwXcaYXT4f4ddVjWiiQs9zowHqyDmvaebN6fU2Lu6iAYnYuepiLkvzGdcZZi8D/0/*,tpubD9cniQzQ8XnuagyP9Xwg3sWCX77wQPWoLPW7jqzcPn37r8hq2X86uztCEyFbMY16amzwdJ1CcNRXhF3vykn1wuDv2ULzryRtaCcN5Cr8F9y/0/*))")] 104 | #[case::multisig_wrapped_watch("multisig_wrapped_watch", 105 | "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))")] 106 | fn descriptor_electrum_wallet_roundtrip(#[case] wallet_name: &str, #[case] descriptor: &str) { 107 | let wallet = ElectrumWalletFile::from_descriptor(descriptor).unwrap(); 108 | 109 | let tempdir = tempdir().unwrap(); 110 | let filename = tempdir.path().join(wallet_name); 111 | wallet.to_file(&filename).unwrap(); 112 | // Testing that electrum can load the files was done manually. 113 | 114 | let imported = ElectrumWalletFile::from_file(&filename).unwrap(); 115 | assert_eq!(wallet, imported); 116 | 117 | let desc = wallet.to_descriptors().unwrap(); 118 | assert_eq!(desc.external, descriptor); 119 | } 120 | 121 | #[rstest] 122 | #[case::default_legacy( 123 | "pkh(tprv8ZgxMBicQKsPeYnCHtn5QZqhTgkkDmXebfQMXWmX7ThXJFCbzDTKFNRsB43GUmHzu2pdGcnnegFy175kFcgZQYC5BFPnRdYDPQyqetpyjb5/0/*)")] 124 | #[case::default_legacy_watch( 125 | "pkh(tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt/0/*)")] 126 | #[case::default_segwit( 127 | "wpkh(tprv8cvkZzx9zA7EfFDbH945mK23r7hg6EHXUk79wVUSRukwyctFS1AdpSpkZcykAMDveCj8RA3R4jwFTKMwMbWexJox8NMqq7YphJLDumfCSfu/0/*)")] 128 | #[case::multisig_hw_segwit( 129 | "wsh(sortedmulti(2,tpubDEcw4ooTbmw62zBKdkYepoP3z4WWugdeRzPHHAbk8XVsPfBE9AAZMNghiqwtdFgtabaeppBTPmezUkRkQZidLcSJp3XTASbMakHcYauWehZ/0/*,tpubDEbkvhmJoZMq3SUNqEf3aEsubvqsCUPc7rroHkGERgS7qA1gQVMxUPrgzth6x43odirLohwf4aMHpvcnWi3jCB2xkizv8T4B2KqLRZVLC6K/0/*))")] 130 | #[case::multisig_legacy( 131 | "sh(sortedmulti(2,tprv8ZgxMBicQKsPeLPWr5WbJDAhANr6irc1Yf7eUNCYjGYap27HU4bDBXWGMT3X75FhDyxNXr6pK4QeHcCBvkqchQzK8wZ4JbGv5X5MWtXQtqy/0/*,tpubD6NzVbkrYhZ4Y1ozBYSfoyVp2iGgP6iZAy18p2opXjVv8jTNccGuRs3jMCMe4ncfwy2RUJsoZLSXsGiFhN47xFbJgtRvCuV3RP3UnxpsrZt/0/*))")] 132 | #[case::multisig_segwit( 133 | "wsh(sortedmulti(2,tprv8dNybiDsdyms39SAWTxyiNHABTTgiqmJpScmxGrdKEuZ7TwXcaYXT4f4ddVjWiiQs9zowHqyDmvaebN6fU2Lu6iAYnYuepiLkvzGdcZZi8D/0/*,tpubD9cniQzQ8XnuagyP9Xwg3sWCX77wQPWoLPW7jqzcPn37r8hq2X86uztCEyFbMY16amzwdJ1CcNRXhF3vykn1wuDv2ULzryRtaCcN5Cr8F9y/0/*))")] 134 | #[case::multisig_wrapped_watch( 135 | "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))")] 136 | fn descriptor_string_roundtrip(#[case] descriptor: &str) { 137 | let wallet = ElectrumWalletFile::from_descriptor(descriptor).unwrap(); 138 | 139 | let electrum_string = wallet.to_string(); 140 | 141 | let imported = ElectrumWalletFile::from_str(&electrum_string).unwrap(); 142 | assert_eq!(wallet, imported); 143 | 144 | let desc = wallet.to_descriptors().unwrap(); 145 | assert_eq!(desc.external, descriptor); 146 | } 147 | --------------------------------------------------------------------------------