Skip to main content

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