1pub mod bip389;
2#[cfg(feature = "bitbox")]
3pub mod bitbox;
4#[cfg(feature = "coldcard")]
5pub mod coldcard;
6#[cfg(feature = "jade")]
7pub mod jade;
8#[cfg(feature = "ledger")]
9pub mod ledger;
10#[cfg(feature = "specter")]
11pub mod specter;
12pub mod utils;
13
14use async_trait::async_trait;
15use bitcoin::{
16 bip32::{ChildNumber, DerivationPath, Fingerprint, Xpub},
17 psbt::Psbt,
18};
19
20use std::{cmp::Ordering, fmt::Debug, str::FromStr};
21
22const RECV_INDEX: ChildNumber = ChildNumber::Normal { index: 0 };
23const CHANGE_INDEX: ChildNumber = ChildNumber::Normal { index: 1 };
24
25#[derive(Debug, Clone)]
26pub enum Error {
27 ParsingPolicy(bip389::ParseError),
28 MissingPolicy,
29 UnsupportedVersion,
30 UnsupportedInput,
31 InvalidParameter(&'static str, String),
32 UnimplementedMethod,
33 DeviceDisconnected,
34 DeviceNotFound,
35 DeviceDidNotSign,
36 Device(String),
37 Unexpected(&'static str),
38 UserRefused,
39 NetworkMismatch,
40 Bip86ChangeIndex,
41}
42
43impl std::fmt::Display for Error {
44 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
45 match self {
46 Error::ParsingPolicy(e) => write!(f, "{}", e),
47 Error::MissingPolicy => write!(f, "Missing policy"),
48 Error::UnsupportedVersion => write!(f, "Unsupported version"),
49 Error::UnsupportedInput => write!(f, "Unsupported input"),
50 Error::UnimplementedMethod => write!(f, "Unimplemented method"),
51 Error::DeviceDisconnected => write!(f, "Device disconnected"),
52 Error::DeviceNotFound => write!(f, "Device not found"),
53 Error::DeviceDidNotSign => write!(f, "Device did not sign"),
54 Error::Device(e) => write!(f, "{}", e),
55 Error::InvalidParameter(param, e) => write!(f, "Invalid parameter {}: {}", param, e),
56 Error::Unexpected(e) => write!(f, "{}", e),
57 Error::UserRefused => write!(f, "User refused operation"),
58 Error::NetworkMismatch => write!(f, "Device network is different"),
59 Error::Bip86ChangeIndex => {
60 write!(f, "Ledger devices only accept 0 or 1 as`change` index value for BIP86 derivation path")
61 }
62 }
63 }
64}
65
66impl From<bip389::ParseError> for Error {
67 fn from(value: bip389::ParseError) -> Self {
68 Error::ParsingPolicy(value)
69 }
70}
71
72impl std::error::Error for Error {}
73
74#[async_trait]
76pub trait HWI: Debug {
77 fn device_kind(&self) -> DeviceKind;
79 async fn get_version(&self) -> Result<Version, Error>;
81 async fn get_master_fingerprint(&self) -> Result<Fingerprint, Error>;
83 async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result<Xpub, Error>;
85 async fn register_wallet(&self, name: &str, policy: &str) -> Result<Option<[u8; 32]>, Error>;
87 async fn is_wallet_registered(&self, name: &str, policy: &str) -> Result<bool, Error>;
89 async fn display_address(&self, script: &AddressScript) -> Result<(), Error>;
91 async fn sign_tx(&self, tx: &mut Psbt) -> Result<(), Error>;
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum AddressScript {
97 P2TR(DerivationPath),
99 Miniscript { index: u32, change: bool },
101}
102
103#[derive(PartialEq, Eq, Debug, Clone, Default)]
104pub struct Version {
105 pub major: u32,
106 pub minor: u32,
107 pub patch: u32,
108 pub prerelease: Option<String>,
109}
110
111#[cfg(feature = "regex")]
112pub fn parse_version(s: &str) -> Result<Version, Error> {
113 let re = regex::Regex::new(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$").unwrap();
115 if let Some(captures) = re.captures(
116 s.trim_start_matches('v')
117 .trim_end_matches("QX")
119 .trim_end_matches('X'),
121 ) {
122 let major = if let Some(s) = captures.get(1) {
123 u32::from_str(s.as_str()).map_err(|_| Error::UnsupportedVersion)?
124 } else {
125 0
126 };
127 let minor = if let Some(s) = captures.get(2) {
128 u32::from_str(s.as_str()).map_err(|_| Error::UnsupportedVersion)?
129 } else {
130 0
131 };
132 let patch = if let Some(s) = captures.get(3) {
133 u32::from_str(s.as_str()).map_err(|_| Error::UnsupportedVersion)?
134 } else {
135 0
136 };
137 Ok(Version {
138 major,
139 minor,
140 patch,
141 prerelease: captures.get(4).map(|s| s.as_str().to_string()),
142 })
143 } else {
144 Err(Error::UnsupportedVersion)
145 }
146}
147
148impl PartialOrd for Version {
149 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
150 match self.major.cmp(&other.major) {
151 Ordering::Equal => match self.minor.cmp(&other.minor) {
152 Ordering::Equal => match self.patch.cmp(&other.patch) {
153 Ordering::Equal => {
154 match (&self.prerelease, &other.prerelease) {
155 (Some(_), Some(_)) => None,
157 (Some(_), None) => Some(Ordering::Greater),
158 (None, Some(_)) => Some(Ordering::Less),
159 (None, None) => Some(Ordering::Equal),
160 }
161 }
162 other => Some(other),
163 },
164 other => Some(other),
165 },
166 other => Some(other),
167 }
168 }
169}
170
171impl std::fmt::Display for Version {
172 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
173 if let Some(prerelease) = &self.prerelease {
174 write!(
175 f,
176 "{}.{}.{}-{}",
177 self.major, self.minor, self.patch, prerelease
178 )
179 } else {
180 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
181 }
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
188pub enum DeviceKind {
189 BitBox02,
190 Coldcard,
191 Specter,
192 SpecterSimulator,
193 Ledger,
194 LedgerSimulator,
195 Jade,
196}
197
198impl std::fmt::Display for DeviceKind {
199 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
200 match self {
201 DeviceKind::BitBox02 => write!(f, "bitbox02"),
202 DeviceKind::Coldcard => write!(f, "coldcard"),
203 DeviceKind::Specter => write!(f, "specter"),
204 DeviceKind::SpecterSimulator => write!(f, "specter-simulator"),
205 DeviceKind::Ledger => write!(f, "ledger"),
206 DeviceKind::LedgerSimulator => write!(f, "ledger-simulator"),
207 DeviceKind::Jade => write!(f, "jade"),
208 }
209 }
210}
211
212impl std::str::FromStr for DeviceKind {
213 type Err = ();
214
215 fn from_str(s: &str) -> Result<Self, Self::Err> {
216 match s {
217 "bitbox02" => Ok(DeviceKind::BitBox02),
218 "specter" => Ok(DeviceKind::Specter),
219 "specter-simulator" => Ok(DeviceKind::SpecterSimulator),
220 "ledger" => Ok(DeviceKind::Ledger),
221 "ledger-simulator" => Ok(DeviceKind::LedgerSimulator),
222 "jade" => Ok(DeviceKind::Jade),
223 _ => Err(()),
224 }
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[cfg(feature = "regex")]
233 #[test]
234 fn test_parse_version() {
235 let test_cases = [
236 (
237 "v2.1.0",
238 Version {
239 major: 2,
240 minor: 1,
241 patch: 0,
242 prerelease: None,
243 },
244 ),
245 (
246 "v1.0",
247 Version {
248 major: 1,
249 minor: 0,
250 patch: 0,
251 prerelease: None,
252 },
253 ),
254 (
255 "3.0-rc2",
256 Version {
257 major: 3,
258 minor: 0,
259 patch: 0,
260 prerelease: Some("rc2".to_string()),
261 },
262 ),
263 (
264 "0.1.0-ALPHA",
265 Version {
266 major: 0,
267 minor: 1,
268 patch: 0,
269 prerelease: Some("ALPHA".to_string()),
270 },
271 ),
272 (
273 "6.2.1X",
274 Version {
275 major: 6,
276 minor: 2,
277 patch: 1,
278 prerelease: None,
279 },
280 ),
281 (
282 "6.3.3QX",
283 Version {
284 major: 6,
285 minor: 3,
286 patch: 3,
287 prerelease: None,
288 },
289 ),
290 ];
291 for (s, v) in test_cases {
292 assert_eq!(v, parse_version(s).unwrap());
293 }
294 }
295
296 #[cfg(feature = "regex")]
297 #[test]
298 fn test_partial_ord_version() {
299 let test_cases = [
300 ("v2.1.0", "v3.1.0"),
301 ("v0.0.1", "v0.1"),
302 ("v0.1", "v1.0.1"),
303 ("v2.0.1", "v2.1.0"),
304 ("v2.1.1", "v3.0-rc1"),
305 ("v3.0-rc1", "v3.0.1"),
306 ("v3.0", "v3.0-rc1"),
307 ];
308 for (l, r) in test_cases {
309 let v1 = parse_version(l).unwrap();
310 let v2 = parse_version(r).unwrap();
311 assert!(v1 < v2);
312 }
313
314 let v1 = parse_version("v2.0-rc1weirdstuff").unwrap();
316 let v2 = parse_version("v2.0-rc1weirderstuff").unwrap();
317 assert!(v1.partial_cmp(&v2).is_none());
318 }
319}