1use std::str::FromStr as _;
2
3use bitcoin::{
4 base58,
5 bip32::{Fingerprint, Xpub as Bip32Xpub},
6};
7
8#[derive(Debug, thiserror::Error)]
9pub enum Error {
10 #[error("Invalid xpub: {0}")]
11 InvalidXpub(#[from] bitcoin::bip32::Error),
12
13 #[error("Invalid zpub: {0}")]
14 InvalidZpub(#[from] base58::Error),
15
16 #[error("Invalid ypub: {0}")]
17 InvalidYpubDecode(base58::Error),
18
19 #[error("Invalid ypub: {0}")]
20 InvalidYpubLength(usize),
21
22 #[error("Not an xpub, zpub or ypub, starts with: {0}")]
23 NotXpub(String),
24
25 #[error("Too short, only {0} chars long")]
26 TooShort(usize),
27
28 #[error("Missing xpub")]
29 MissingXpub,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Xpub {
34 xpub: Bip32Xpub,
35 original_format: OriginalFormat,
36}
37
38impl std::fmt::Display for Xpub {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(f, "{}", self.xpub)
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)]
45pub enum OriginalFormat {
46 Zpub,
47 Ypub,
48 Xpub,
49}
50
51impl Xpub {
52 pub fn master_fingerprint(&self) -> Option<Fingerprint> {
53 let fingerprint = xpub_to_fingerprint(&self.xpub).ok()?;
54 if fingerprint == Fingerprint::default() {
55 return None;
56 }
57
58 Some(fingerprint)
59 }
60
61 pub fn fingerprint(&self) -> Fingerprint {
62 self.xpub.fingerprint()
63 }
64}
65
66impl TryFrom<&str> for Xpub {
67 type Error = Error;
68
69 fn try_from(xpub: &str) -> Result<Self, Self::Error> {
70 let (xpub, original_format) = match &xpub[..4] {
71 "zpub" => (zpub_to_xpub(xpub)?, OriginalFormat::Zpub),
72 "ypub" => (ypub_to_xpub(xpub)?, OriginalFormat::Ypub),
73 "xpub" => (xpub.to_string(), OriginalFormat::Xpub),
74 starting => return Err(Error::NotXpub(starting.to_string())),
75 };
76
77 Ok(Self {
78 xpub: Bip32Xpub::from_str(&xpub)?,
79 original_format,
80 })
81 }
82}
83
84pub fn zpub_to_xpub(zpub: &str) -> Result<String, Error> {
85 let decoded = base58::decode_check(zpub)?;
86
87 let mut xpub_bytes = [0u8; 78];
89 xpub_bytes[0..4].copy_from_slice(&[0x04, 0x88, 0xB2, 0x1E]); xpub_bytes[4..].copy_from_slice(&decoded[4..]);
91
92 let xpub = base58::encode_check(&xpub_bytes);
94
95 Ok(xpub)
96}
97
98pub fn ypub_to_xpub(ypub: &str) -> Result<String, Error> {
99 let decoded = base58::decode_check(ypub).map_err(Error::InvalidYpubDecode)?;
100
101 if decoded.len() != 78 {
102 return Err(Error::InvalidYpubLength(decoded.len()));
103 }
104
105 let mut xpub_bytes = [0u8; 78];
106 xpub_bytes.copy_from_slice(&decoded);
107 xpub_bytes[0..4].copy_from_slice(&[0x04, 0x88, 0xB2, 0x1E]); let xpub = base58::encode_check(&xpub_bytes);
111
112 Ok(xpub)
113}
114
115pub fn xpub_to_fingerprint(xpub: &Bip32Xpub) -> Result<Fingerprint, Error> {
116 let fingerprint = match xpub.parent_fingerprint.as_bytes() {
117 [0, 0, 0, 0] => xpub.fingerprint(),
118 _ => xpub.parent_fingerprint,
119 };
120 Ok(fingerprint)
121}
122
123pub fn xpub_str_to_fingerprint(xpub: &str) -> Result<Fingerprint, Error> {
124 let xpub = Bip32Xpub::from_str(xpub)?;
125 let fingerprint = xpub_to_fingerprint(&xpub)?;
126 Ok(fingerprint)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use pretty_assertions::assert_eq;
133
134 #[test]
135 fn test_zpub_to_xpub() {
136 let zpub = "zpub6rNrPrFwgm4wMBSysetK5tpLBS2HYT8TDKQA6amxFHKJUnQq8rNtc4JDfGYPbvF9wJyagPpG1Faqnfe3BB8XzKon8LwW9KkMWyAQ4RQHzB1";
137 let xpub_str = "xpub6CiKnWv7PPyyeb4kCwK4fidKqVjPfD9TP6MiXnzBVGZYNanNdY3mMvywcrdDc6wK82jyBSd95vsk26QujnJWPrSaPfYeyW7NyX37HHGtfQM";
138 let xpub = Xpub::try_from(zpub);
139
140 assert!(xpub.is_ok());
141 let xpub = xpub.unwrap();
142
143 assert_eq!(xpub.xpub.to_string(), xpub_str);
144 }
145
146 #[test]
147 fn test_ypub_to_xpub() {
148 let ypub = "ypub6X2aUb9NXbQM65mQy6oFECSB1CdSanwXHGTUcw7vt2LaAteuYtLoDQ6ao1fXDsenrZjgJKJyHvLypBBeo59cSKUivvwW8S6k7PVvQkVosxZ";
149 let xpub_str = "xpub6CCKAvUTNursEnaJ8k1d27LfqEUzeAx2N9wFqYE3W1xh7nqgJEBEbLSSmohwDxzsSvcsYqiQqFzRvta65Njbe5o84bF5YXHFqfSH2Dkhonm";
150 let xpub = Xpub::try_from(ypub);
151
152 assert!(xpub.is_ok());
153 let xpub = xpub.unwrap();
154
155 assert_eq!(xpub.xpub.to_string().as_str(), xpub_str);
156 }
157
158 #[test]
159 fn test_zpub_to_xpub_direct() {
160 let zpub = "zpub6rNrPrFwgm4wMBSysetK5tpLBS2HYT8TDKQA6amxFHKJUnQq8rNtc4JDfGYPbvF9wJyagPpG1Faqnfe3BB8XzKon8LwW9KkMWyAQ4RQHzB1";
162 let expected = "xpub6CiKnWv7PPyyeb4kCwK4fidKqVjPfD9TP6MiXnzBVGZYNanNdY3mMvywcrdDc6wK82jyBSd95vsk26QujnJWPrSaPfYeyW7NyX37HHGtfQM";
163
164 let result = zpub_to_xpub(zpub).expect("should convert zpub to xpub");
165 assert_eq!(result, expected);
166 }
167
168 #[test]
169 fn test_ypub_to_xpub_direct() {
170 let ypub = "ypub6X2aUb9NXbQM65mQy6oFECSB1CdSanwXHGTUcw7vt2LaAteuYtLoDQ6ao1fXDsenrZjgJKJyHvLypBBeo59cSKUivvwW8S6k7PVvQkVosxZ";
171 let expected = "xpub6CCKAvUTNursEnaJ8k1d27LfqEUzeAx2N9wFqYE3W1xh7nqgJEBEbLSSmohwDxzsSvcsYqiQqFzRvta65Njbe5o84bF5YXHFqfSH2Dkhonm";
172
173 let result = ypub_to_xpub(ypub).expect("should convert ypub to xpub");
174 assert_eq!(result, expected);
175 }
176
177 #[test]
178 fn test_zpub_to_xpub_invalid() {
179 let invalid = "zpubINVALID";
180 let result = zpub_to_xpub(invalid);
181 assert!(result.is_err());
182 }
183
184 #[test]
185 fn test_ypub_to_xpub_invalid() {
186 let invalid = "ypubINVALID";
187 let result = ypub_to_xpub(invalid);
188 assert!(result.is_err());
189 }
190}