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