Skip to content

Commit

Permalink
added types for returning descriptors and errors
Browse files Browse the repository at this point in the history
  • Loading branch information
ulrichard committed Nov 20, 2024
1 parent e12944d commit 6c94474
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 90 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ jobs:
with:
toolchain: ${{ matrix.rust }}
components: rustfmt, clippy
- name: Pin dependencies for MSRV
if: matrix.rust <= '1.63.0'
run: |
cargo update -p cc --precise 1.0.82
cargo update -p regex --precise 1.9.0
cargo update -p regex-automata --precise 0.3.0
cargo update -p tokio --precise 1.36.0
cargo update -p libc --precise 0.2.154
- name: Build
run: cargo build --no-default-features --features "${{ matrix.features }}"
- name: Run tests
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "electrum2descriptors"
version = "0.5.0"
version = "0.6.0"
authors = ["Riccardo Casatta <riccardo@casatta.it>"]
edition = "2018"
description = "Converts electrum xpubs (like vpub, ypub...) into output descriptors"
Expand All @@ -23,6 +23,7 @@ path = "src/bin.rs"

[dependencies]
bitcoin = "0.30"
thiserror = "2"

# Optional dependencies
serde = { version = "1", optional = true, features = ["derive"] }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Converts [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.

This project consists of a library and an executable.

The work of @ulrichard in this project was sponsored by [SEBA Bank AG](https://seba.swiss)
The work of @ulrichard in this project was sponsored by [AMINA Bank AG](https://aminagroup.com)

## Usage library
For the library interface read [the docs](https://docs.rs/electrum2descriptors/latest/libelectrum2descriptors/).
Expand Down
12 changes: 7 additions & 5 deletions src/bin.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
#[cfg(feature = "wallet_file")]
use libelectrum2descriptors::ElectrumWalletFile;
use libelectrum2descriptors::{
ElectrumExtendedKey, ElectrumExtendedPrivKey, ElectrumExtendedPubKey,
Electrum2DescriptorError, ElectrumExtendedKey, ElectrumExtendedPrivKey, ElectrumExtendedPubKey,
};
#[cfg(feature = "wallet_file")]
use std::path::Path;
use std::str::FromStr;

fn main() -> Result<(), String> {
fn main() -> Result<(), Electrum2DescriptorError> {
let mut args = std::env::args();
args.next(); // first is program name
let err_msg =
"You must specify an extended public or private key or an electrum wallet file as first argument".to_string();
let electrum_x = args.next().ok_or_else(|| err_msg.clone())?;
let electrum_x = args
.next()
.ok_or_else(|| Electrum2DescriptorError::Generic(err_msg.clone()))?;
let descriptor = ElectrumExtendedPrivKey::from_str(&electrum_x)
.map(|e| e.to_descriptors())
.or_else(|_| ElectrumExtendedPubKey::from_str(&electrum_x).map(|e| e.to_descriptors()));
#[cfg(feature = "wallet_file")]
let descriptor = descriptor.or_else(|_| {
let wallet_file = Path::new(&electrum_x)
.canonicalize()
.map_err(|_| err_msg.clone())?;
.map_err(|_| Electrum2DescriptorError::Generic(err_msg.clone()))?;
if !wallet_file.exists() {
return Err(err_msg);
return Err(Electrum2DescriptorError::Generic(err_msg));
}
let wallet = ElectrumWalletFile::from_file(wallet_file.as_path())?;
wallet.to_descriptors()
Expand Down
37 changes: 22 additions & 15 deletions src/electrum_extended_priv_key.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::ElectrumExtendedKey;
use crate::{Descriptors, Electrum2DescriptorError, ElectrumExtendedKey};
use bitcoin::base58;
use bitcoin::bip32::{ChainCode, ChildNumber, ExtendedPrivKey, Fingerprint};
use bitcoin::secp256k1;
Expand Down Expand Up @@ -87,19 +87,21 @@ fn initialize_sentinels() -> SentinelMap {
}

impl FromStr for ElectrumExtendedPrivKey {
type Err = String;
type Err = Electrum2DescriptorError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let data = base58::decode_check(s).map_err(|e| e.to_string())?;
let data = base58::decode_check(s)?;

if data.len() != 78 {
return Err(base58::Error::InvalidLength(data.len()).to_string());
return Err(Electrum2DescriptorError::Base58Error(
base58::Error::InvalidLength(data.len()),
));
}

let cn_int = u32::from_be_bytes(data[9..13].try_into().unwrap());
let child_number: ChildNumber = ChildNumber::from(cn_int);
let (network, kind) = match_electrum_xprv(&data[0..4]).map_err(|e| e.to_string())?;
let key = secp256k1::SecretKey::from_slice(&data[46..78]).map_err(|e| e.to_string())?;
let (network, kind) = match_electrum_xprv(&data[0..4])?;
let key = secp256k1::SecretKey::from_slice(&data[46..78])?;

let xprv = ExtendedPrivKey {
network,
Expand All @@ -125,12 +127,15 @@ impl ElectrumExtendedKey for ElectrumExtendedPrivKey {
}

/// Returns internal and external descriptor
fn to_descriptors(&self) -> Vec<String> {
fn to_descriptors(&self) -> Descriptors {
let xprv = self.xprv.to_string();
let closing_parenthesis = if self.kind.contains('(') { ")" } else { "" };
(0..=1)
.map(|i| format!("{}({}/{}/*){}", self.kind, xprv, i, closing_parenthesis))
.collect()
let descriptors =
[0, 1].map(|i| format!("{}({}/{}/*){}", self.kind, xprv, i, closing_parenthesis));
Descriptors {
external: descriptors[0].clone(),
change: descriptors[1].clone(),
}
}
}

Expand All @@ -146,12 +151,12 @@ impl ElectrumExtendedPrivKey {
}

/// converts to electrum format
pub fn electrum_xprv(&self) -> Result<String, String> {
pub fn electrum_xprv(&self) -> Result<String, Electrum2DescriptorError> {
let sentinels = initialize_sentinels();
let sentinel = sentinels
.iter()
.find(|sent| sent.1 == self.xprv.network && sent.2 == self.kind)
.ok_or_else(|| "unknown type".to_string())?;
.ok_or_else(|| Electrum2DescriptorError::UnknownType)?;
let mut data = Vec::from(&sentinel.0[..]);
data.push(self.xprv.depth);
data.extend(self.xprv.parent_fingerprint.as_bytes());
Expand All @@ -162,7 +167,9 @@ impl ElectrumExtendedPrivKey {
data.extend(self.xprv.private_key.as_ref());

if data.len() != 78 {
return Err(base58::Error::InvalidLength(data.len()).to_string());
return Err(Electrum2DescriptorError::Base58Error(
base58::Error::InvalidLength(data.len()),
));
}

Ok(base58::encode_check(&data))
Expand Down Expand Up @@ -191,8 +198,8 @@ mod tests {
assert_eq!(electrum_xprv.xprv.to_string(),"xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD");
assert_eq!(electrum_xprv.kind, "sh(wpkh");
let descriptors = electrum_xprv.to_descriptors();
assert_eq!(descriptors[0], "sh(wpkh(xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD/0/*))");
assert_eq!(descriptors[1], "sh(wpkh(xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD/1/*))");
assert_eq!(descriptors.external, "sh(wpkh(xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD/0/*))");
assert_eq!(descriptors.change, "sh(wpkh(xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD/1/*))");
let xprv = electrum_xprv.xprv();
assert_eq!(xprv.to_string(), "xprv9y7S1RkggDtZnP1RSzJ7PwUR4MUfF66Wz2jGv9TwJM52WLGmnnrQLLzBSTi7rNtBk4SGeQHBj5G4CuQvPXSn58BmhvX9vk6YzcMm37VuNYD");
}
Expand Down
40 changes: 23 additions & 17 deletions src/electrum_extended_pub_key.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::ElectrumExtendedKey;
use crate::{Descriptors, Electrum2DescriptorError, ElectrumExtendedKey};
use bitcoin::base58;
use bitcoin::bip32::{ChainCode, ChildNumber, ExtendedPubKey, Fingerprint};
use bitcoin::secp256k1;
Expand Down Expand Up @@ -87,27 +87,28 @@ fn initialize_sentinels() -> SentinelMap {
}

impl FromStr for ElectrumExtendedPubKey {
type Err = String;
type Err = Electrum2DescriptorError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let data = base58::decode_check(s).map_err(|e| e.to_string())?;
let data = base58::decode_check(s)?;

if data.len() != 78 {
return Err(base58::Error::InvalidLength(data.len()).to_string());
return Err(Electrum2DescriptorError::Base58Error(
base58::Error::InvalidLength(data.len()),
));
}

let cn_int = u32::from_be_bytes(data[9..13].try_into().unwrap());
let child_number: ChildNumber = ChildNumber::from(cn_int);
let (network, kind) = match_electrum_xpub(&data[0..4]).map_err(|e| e.to_string())?;
let (network, kind) = match_electrum_xpub(&data[0..4])?;

let xpub = ExtendedPubKey {
network,
depth: data[4],
parent_fingerprint: Fingerprint::from(&data[5..9].try_into().unwrap()),
child_number,
chain_code: ChainCode::from(&data[13..45].try_into().unwrap()),
public_key: secp256k1::PublicKey::from_slice(&data[45..78])
.map_err(|e| e.to_string())?,
public_key: secp256k1::PublicKey::from_slice(&data[45..78])?,
};
Ok(ElectrumExtendedPubKey { xpub, kind })
}
Expand All @@ -125,12 +126,15 @@ impl ElectrumExtendedKey for ElectrumExtendedPubKey {
}

/// Returns internal and external descriptor
fn to_descriptors(&self) -> Vec<String> {
fn to_descriptors(&self) -> Descriptors {
let xpub = self.xpub.to_string();
let closing_parenthesis = if self.kind.contains('(') { ")" } else { "" };
(0..=1)
.map(|i| format!("{}({}/{}/*){}", self.kind, xpub, i, closing_parenthesis))
.collect()
let descriptors =
[0, 1].map(|i| format!("{}({}/{}/*){}", self.kind, xpub, i, closing_parenthesis));
Descriptors {
external: descriptors[0].clone(),
change: descriptors[1].clone(),
}
}
}

Expand All @@ -146,12 +150,12 @@ impl ElectrumExtendedPubKey {
}

/// converts to electrum format
pub fn electrum_xpub(&self) -> Result<String, String> {
pub fn electrum_xpub(&self) -> Result<String, Electrum2DescriptorError> {
let sentinels = initialize_sentinels();
let sentinel = sentinels
.iter()
.find(|sent| sent.1 == self.xpub.network && sent.2 == self.kind)
.ok_or_else(|| "unknown type".to_string())?;
.ok_or_else(|| Electrum2DescriptorError::UnknownType)?;
let mut data = Vec::from(&sentinel.0[..]);
data.push(self.xpub.depth);
data.extend(self.xpub.parent_fingerprint.as_bytes());
Expand All @@ -161,7 +165,9 @@ impl ElectrumExtendedPubKey {
data.extend(&self.xpub.public_key.serialize()); // or serialize_uncompressed

if data.len() != 78 {
return Err(base58::Error::InvalidLength(data.len()).to_string());
return Err(Electrum2DescriptorError::Base58Error(
base58::Error::InvalidLength(data.len()),
));
}

Ok(base58::encode_check(&data))
Expand Down Expand Up @@ -192,8 +198,8 @@ mod tests {
assert_eq!(electrum_xpub.xpub.to_string(),"tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp");
assert_eq!(electrum_xpub.kind, "wpkh");
let descriptors = electrum_xpub.to_descriptors();
assert_eq!(descriptors[0], "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/0/*)");
assert_eq!(descriptors[1], "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/1/*)");
assert_eq!(descriptors.external, "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/0/*)");
assert_eq!(descriptors.change, "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/1/*)");
let xpub = electrum_xpub.xpub();
assert_eq!(xpub.to_string(), "tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp");
}
Expand Down Expand Up @@ -230,7 +236,7 @@ mod tests {
assert_eq!(electrum_xpub.xpub.network, Network::Bitcoin);
let descriptors = electrum_xpub.to_descriptors();
let descriptor: miniscript::Descriptor<DescriptorPublicKey> =
descriptors[0].parse().unwrap();
descriptors.external.parse().unwrap();
let secp = Secp256k1::verification_only();
let first_address = descriptor
.at_derivation_index(0)
Expand Down
Loading

0 comments on commit 6c94474

Please sign in to comment.