Skip to content

Commit

Permalink
Add coldcard feature
Browse files Browse the repository at this point in the history
  • Loading branch information
edouardparis committed Nov 18, 2023
1 parent 605126e commit 00009f0
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 82 deletions.
11 changes: 8 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,18 @@ authors.workspace = true
repository.workspace = true

[features]
default = ["ledger", "specter", "bitbox"]
bitbox = ["tokio", "hidapi", "bitbox-api", "regex" ]
default = ["ledger", "specter", "coldcard", "bitbox"]
bitbox = ["tokio", "hidapi", "bitbox-api", "regex"]
coldcard = ["dep:coldcard", "regex"]
specter = ["tokio", "tokio-serial", "serialport"]
ledger = ["regex", "tokio", "ledger_bitcoin_client", "ledger-transport-hidapi", "ledger-apdu", "hidapi"]
regex = ["dep:regex"]

[dependencies]
base64 = "0.13.0"
async-trait = "0.1.52"
futures = "0.3"
bitcoin = { version = "0.30.0", default-features = false, features = ["base64", "serde", "no-std"] }
bitcoin = { version = "0.30.0", default-features = false, features = ["base64", "serde", "std"] }

# specter
tokio-serial = { version = "5.4.1", optional = true }
Expand All @@ -41,6 +43,9 @@ serialport = { version = "4.2", optional = true }
#bitbox
bitbox-api = { version = "0.2.2", default-features = false, features = ["usb", "tokio", "multithreaded"], optional = true }

#coldcard
coldcard = { git = "https://github.com/edouardparis/rust-coldcard", branch = "miniscript-enroll", optional = true }

# ledger
ledger_bitcoin_client = { version = "0.3.2", optional = true }
ledger-apdu = { version = "0.10", optional = true }
Expand Down
12 changes: 11 additions & 1 deletion cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod command {
use async_hwi::{
bitbox::{api::runtime, BitBox02, PairingBitbox02WithLocalCache},
coldcard,
ledger::{HidApi, Ledger, LedgerSimulator, TransportHID},
specter::{Specter, SpecterSimulator},
HWI,
Expand Down Expand Up @@ -34,7 +35,7 @@ pub mod command {
hws.push(device.into());
}

let api = HidApi::new().unwrap();
let api = Box::new(HidApi::new().unwrap());

for device_info in api.device_list() {
if async_hwi::bitbox::is_bitbox02(device_info) {
Expand All @@ -55,6 +56,15 @@ pub mod command {
}
}
}
if device_info.vendor_id() == coldcard::api::COINKITE_VID
&& device_info.product_id() == coldcard::api::CKCC_PID
{
if let Some(sn) = device_info.serial_number() {
if let Ok((cc, _)) = coldcard::api::Coldcard::open(&api, sn, None) {
hws.push(coldcard::Coldcard::from(cc).into())
}
}
}
}

for detected in Ledger::<TransportHID>::enumerate(&api) {
Expand Down
102 changes: 102 additions & 0 deletions src/coldcard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use std::{
str::FromStr,
sync::{Arc, Mutex},
};

use async_trait::async_trait;
use bitcoin::{
bip32::{DerivationPath, ExtendedPubKey, Fingerprint},
psbt::Psbt,
};

use crate::{parse_version, AddressScript, DeviceKind, Error as HWIError, Version, HWI};
pub use coldcard as api;

#[derive(Debug)]
pub struct Coldcard(Arc<Mutex<coldcard::Coldcard>>);

impl From<coldcard::Coldcard> for Coldcard {
fn from(cc: coldcard::Coldcard) -> Self {
Coldcard(Arc::new(Mutex::new(cc)))
}
}

#[async_trait]
impl HWI for Coldcard {
fn device_kind(&self) -> DeviceKind {
DeviceKind::Coldcard
}

/// The first semver version returned by coldcard is the firmware version.
async fn get_version(&self) -> Result<Version, HWIError> {
let mut cc = self
.0
.lock()
.map_err(|_| HWIError::Unexpected("Failed to unlock"))?;
let s = cc.version().map_err(|e| HWIError::Device(e.to_string()))?;
for line in s.split('\n') {
if let Ok(version) = parse_version(line) {
return Ok(version);
}
}
Err(HWIError::UnsupportedVersion)
}

async fn get_master_fingerprint(&self) -> Result<Fingerprint, HWIError> {
let mut cc = self
.0
.lock()
.map_err(|_| HWIError::Unexpected("Failed to unlock"))?;
let s = cc.xpub(None).map_err(|e| HWIError::Device(e.to_string()))?;
let xpub = ExtendedPubKey::from_str(&s).map_err(|e| HWIError::Device(e.to_string()))?;
Ok(xpub.fingerprint())
}

async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result<ExtendedPubKey, HWIError> {
let path = coldcard::protocol::DerivationPath::new(&path.to_string())
.map_err(|e| HWIError::InvalidParameter("path", format!("{:?}", e)))?;
let mut cc = self
.0
.lock()
.map_err(|_| HWIError::Unexpected("Failed to unlock"))?;
let s = cc
.xpub(Some(path))
.map_err(|e| HWIError::Device(e.to_string()))?;
ExtendedPubKey::from_str(&s).map_err(|e| HWIError::Device(e.to_string()))
}

async fn display_address(&self, _script: &AddressScript) -> Result<(), HWIError> {
Err(HWIError::UnimplementedMethod)
}

async fn register_wallet(
&self,
_name: &str,
policy: &str,
) -> Result<Option<[u8; 32]>, HWIError> {
let mut cc = self
.0
.lock()
.map_err(|_| HWIError::Unexpected("Failed to unlock"))?;
let _ = cc
.miniscript_enroll(policy.as_bytes())
.map_err(|e| HWIError::Device(e.to_string()))?;
Ok(None)
}

async fn sign_tx(&self, _tx: &mut Psbt) -> Result<(), HWIError> {
Err(HWIError::UnimplementedMethod)
}
}

impl From<Coldcard> for Box<dyn HWI + Send> {
fn from(s: Coldcard) -> Box<dyn HWI + Send> {
Box::new(s)
}
}

impl From<Coldcard> for Arc<dyn HWI + Sync + Send> {
fn from(s: Coldcard) -> Arc<dyn HWI + Sync + Send> {
Arc::new(s)
}
}
79 changes: 2 additions & 77 deletions src/ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use ledger_bitcoin_client::{
WalletPolicy, WalletPubKey,
};

use crate::{utils, AddressScript, DeviceKind, Error as HWIError, Version, HWI};
use crate::{parse_version, utils, AddressScript, DeviceKind, Error as HWIError, HWI};

pub use hidapi::{DeviceInfo, HidApi};
pub use ledger_bitcoin_client::async_client::Transport;
Expand Down Expand Up @@ -85,7 +85,7 @@ impl<T: Transport + Sync + Send> HWI for Ledger<T> {

async fn get_version(&self) -> Result<super::Version, HWIError> {
let (_, version, _) = self.client.get_version().await?;
Ok(extract_version(&version)?)
Ok(parse_version(&version)?)
}

async fn get_master_fingerprint(&self) -> Result<Fingerprint, HWIError> {
Expand Down Expand Up @@ -207,36 +207,6 @@ pub fn extract_keys_and_template(policy: &str) -> Result<(String, Vec<WalletPubK
}
}

pub fn extract_version(s: &str) -> Result<Version, HWIError> {
// Regex from https://semver.org/ with patch group marked as optional
let re = 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();
if let Some(captures) = re.captures(s.trim_start_matches('v')) {
let major = if let Some(s) = captures.get(1) {
u32::from_str(s.as_str()).map_err(|_| HWIError::UnsupportedVersion)?
} else {
0
};
let minor = if let Some(s) = captures.get(2) {
u32::from_str(s.as_str()).map_err(|_| HWIError::UnsupportedVersion)?
} else {
0
};
let patch = if let Some(s) = captures.get(3) {
u32::from_str(s.as_str()).map_err(|_| HWIError::UnsupportedVersion)?
} else {
0
};
Ok(Version {
major,
minor,
patch,
prerelease: captures.get(4).map(|s| s.as_str().to_string()),
})
} else {
Err(HWIError::UnsupportedVersion)
}
}

impl Ledger<TransportHID> {
pub fn enumerate(api: &HidApi) -> impl Iterator<Item = &DeviceInfo> {
TransportNativeHID::list_ledgers(api)
Expand Down Expand Up @@ -371,49 +341,4 @@ mod tests {
assert_eq!(res.1[1].to_string(), "[7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ".to_string());
assert_eq!(res.1[2].to_string(), "[1a1ffd98/48'/1'/0'/2']tpubDFZqzTvGijYb13BC73CkS1er8DrP5YdzMhziN3kWCKUFaW51Yj6ggvf99YpdrkTJy4RT85mxQMHXDiFAKRxzf6BykQgT4pRRBNPshSJJcKo".to_string());
}

#[test]
fn test_extract_version() {
let test_cases = [
(
"v2.1.0",
Version {
major: 2,
minor: 1,
patch: 0,
prerelease: None,
},
),
(
"v1.0",
Version {
major: 1,
minor: 0,
patch: 0,
prerelease: None,
},
),
(
"3.0-rc2",
Version {
major: 3,
minor: 0,
patch: 0,
prerelease: Some("rc2".to_string()),
},
),
(
"0.1.0-ALPHA",
Version {
major: 0,
minor: 1,
patch: 0,
prerelease: Some("ALPHA".to_string()),
},
),
];
for (s, v) in test_cases {
assert_eq!(v, extract_version(s).unwrap());
}
}
}
Loading

0 comments on commit 00009f0

Please sign in to comment.