Skip to main content

async_hwi/
coldcard.rs

1use std::{
2    str::FromStr,
3    sync::{Arc, Mutex, MutexGuard},
4};
5
6use async_trait::async_trait;
7use bitcoin::{
8    bip32::{DerivationPath, Fingerprint, Xpub},
9    psbt::Psbt,
10};
11use hidapi::DeviceInfo;
12
13use crate::{parse_version, AddressScript, DeviceKind, Error as HWIError, Version, HWI};
14pub use coldcard as api;
15
16#[derive(Debug)]
17pub struct Coldcard {
18    device: Arc<Mutex<coldcard::Coldcard>>,
19    wallet_name: Option<String>,
20}
21
22impl Coldcard {
23    pub fn with_wallet_name(mut self, wallet_name: String) -> Self {
24        self.wallet_name = Some(wallet_name);
25        self
26    }
27
28    fn device(&self) -> Result<MutexGuard<'_, coldcard::Coldcard>, HWIError> {
29        self.device
30            .lock()
31            .map_err(|_| HWIError::Unexpected("Failed to unlock"))
32    }
33}
34
35impl From<coldcard::Coldcard> for Coldcard {
36    fn from(cc: coldcard::Coldcard) -> Self {
37        Coldcard {
38            device: Arc::new(Mutex::new(cc)),
39            wallet_name: None,
40        }
41    }
42}
43
44#[async_trait]
45impl HWI for Coldcard {
46    fn device_kind(&self) -> DeviceKind {
47        DeviceKind::Coldcard
48    }
49
50    /// The first semver version returned by coldcard is the firmware version.
51    async fn get_version(&self) -> Result<Version, HWIError> {
52        let s = self.device()?.version()?;
53        for line in s.split('\n') {
54            if let Ok(version) = parse_version(line) {
55                return Ok(version);
56            }
57        }
58        Err(HWIError::UnsupportedVersion)
59    }
60
61    async fn get_master_fingerprint(&self) -> Result<Fingerprint, HWIError> {
62        let s = self.device()?.xpub(None)?;
63        let xpub = Xpub::from_str(&s).map_err(|e| HWIError::Device(e.to_string()))?;
64        Ok(xpub.fingerprint())
65    }
66
67    async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result<Xpub, HWIError> {
68        let path = path.to_string();
69        let path = if path.starts_with("m/") {
70            path
71        } else {
72            format!("m/{}", path)
73        };
74        let path = coldcard::protocol::DerivationPath::new(&path)
75            .map_err(|e| HWIError::InvalidParameter("path", format!("{:?}", e)))?;
76        let s = self.device()?.xpub(Some(path))?;
77        Xpub::from_str(&s).map_err(|e| HWIError::Device(e.to_string()))
78    }
79
80    async fn display_address(&self, script: &AddressScript) -> Result<(), HWIError> {
81        if let Some(name) = &self.wallet_name {
82            let descriptor_name = coldcard::protocol::DescriptorName::new(name)
83                .map_err(|_| HWIError::UnsupportedInput)?;
84            if let AddressScript::Miniscript { index, change } = script {
85                self.device()?
86                    .miniscript_address(descriptor_name, *change, *index)?;
87                Ok(())
88            } else {
89                Err(HWIError::UnimplementedMethod)
90            }
91        } else {
92            Err(HWIError::UnimplementedMethod)
93        }
94    }
95
96    async fn register_wallet(
97        &self,
98        name: &str,
99        policy: &str,
100    ) -> Result<Option<[u8; 32]>, HWIError> {
101        let payload = format!("{{\"name\":\"{}\",\"desc\":\"{}\"}}", name, policy);
102        let _ = self.device()?.miniscript_enroll(payload.as_bytes())?;
103        Ok(None)
104    }
105
106    async fn is_wallet_registered(&self, name: &str, policy: &str) -> Result<bool, HWIError> {
107        let descriptor_name = coldcard::protocol::DescriptorName::new(name)
108            .map_err(|_| HWIError::UnsupportedInput)?;
109        let desc = self.device()?.miniscript_get(descriptor_name)?;
110        if let Some(desc) = desc {
111            if let Some((policy, _)) = policy.replace('\'', "h").split_once('#') {
112                Ok(desc.contains(policy))
113            } else {
114                Ok(desc.contains(policy))
115            }
116        } else {
117            Ok(false)
118        }
119    }
120
121    async fn sign_tx(&self, psbt: &mut Psbt) -> Result<(), HWIError> {
122        let mut cc = self.device()?;
123
124        let _ = cc.sign_psbt(&psbt.serialize(), api::SignMode::Signed)?;
125
126        let tx = loop {
127            if let Some(tx) = cc.get_signed_tx()? {
128                break tx;
129            }
130        };
131
132        let mut new_psbt = Psbt::deserialize(&tx).map_err(|e| HWIError::Device(e.to_string()))?;
133
134        for i in 0..new_psbt.inputs.len() {
135            psbt.inputs[i]
136                .partial_sigs
137                .append(&mut new_psbt.inputs[i].partial_sigs);
138            psbt.inputs[i]
139                .tap_script_sigs
140                .append(&mut new_psbt.inputs[i].tap_script_sigs);
141            if let Some(sig) = new_psbt.inputs[i].tap_key_sig {
142                psbt.inputs[i].tap_key_sig = Some(sig);
143            }
144        }
145
146        Ok(())
147    }
148}
149
150impl From<api::Error> for HWIError {
151    fn from(e: api::Error) -> Self {
152        if let api::Error::UnexpectedResponse(api::protocol::Response::Refused) = e {
153            HWIError::UserRefused
154        } else {
155            HWIError::Device(e.to_string())
156        }
157    }
158}
159
160impl From<Coldcard> for Box<dyn HWI + Send> {
161    fn from(s: Coldcard) -> Box<dyn HWI + Send> {
162        Box::new(s)
163    }
164}
165
166impl From<Coldcard> for Arc<dyn HWI + Sync + Send> {
167    fn from(s: Coldcard) -> Arc<dyn HWI + Sync + Send> {
168        Arc::new(s)
169    }
170}
171
172pub fn is_coldcard(device_info: &DeviceInfo) -> bool {
173    device_info.vendor_id() == api::COINKITE_VID && device_info.product_id() == api::CKCC_PID
174}