async_hwi/
lib.rs

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/// HWI is the common Hardware Wallet Interface.
75#[async_trait]
76pub trait HWI: Debug {
77    /// Return the device kind
78    fn device_kind(&self) -> DeviceKind;
79    /// Application version or OS version.
80    async fn get_version(&self) -> Result<Version, Error>;
81    /// Get master fingerprint.
82    async fn get_master_fingerprint(&self) -> Result<Fingerprint, Error>;
83    /// Get the xpub with the given derivation path.
84    async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result<Xpub, Error>;
85    /// Register a new wallet policy.
86    async fn register_wallet(&self, name: &str, policy: &str) -> Result<Option<[u8; 32]>, Error>;
87    /// Returns true if the wallet is registered on the device.
88    async fn is_wallet_registered(&self, name: &str, policy: &str) -> Result<bool, Error>;
89    /// Display address on the device screen.
90    async fn display_address(&self, script: &AddressScript) -> Result<(), Error>;
91    /// Sign a partially signed bitcoin transaction (PSBT).
92    async fn sign_tx(&self, tx: &mut Psbt) -> Result<(), Error>;
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum AddressScript {
97    /// Must be a bip86 path.
98    P2TR(DerivationPath),
99    /// Miniscript requires the policy be loaded into the device.
100    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    // Regex from https://semver.org/ with patch group marked as optional
114    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            // Coldcard Q does not follow semver format
118            .trim_end_matches("QX")
119            // Coldcard mk4 does not follow semver format
120            .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                            // Cannot compare versions at this point.
156                            (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/// DeviceType is the result of the following process:
186/// If it is talking like a Duck© hardware wallet it is a Duck© hardware wallet.
187#[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        // We cannot compare prerelease of the same version.
315        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}