1use std::convert::TryFrom;
2use std::default::Default;
3use std::error::Error;
4use std::net::{IpAddr, Ipv4Addr, SocketAddr};
5
6use async_trait::async_trait;
7use bitcoin::{
8 bip32::{DerivationPath, Fingerprint, Xpub},
9 psbt::Psbt,
10};
11use ledger_bitcoin_client::psbt::PartialSignature;
12
13use ledger_apdu::APDUAnswer;
14use ledger_transport_hidapi::TransportNativeHID;
15use tokio::{
16 io::{AsyncReadExt, AsyncWriteExt},
17 net::TcpStream,
18 sync::Mutex,
19};
20
21use ledger_bitcoin_client::{
22 apdu::{APDUCommand, StatusWord},
23 async_client::BitcoinClient,
24 error::BitcoinClientError,
25 wallet::Version as WalletVersion,
26 WalletPolicy, WalletPubKey,
27};
28
29use crate::{
30 parse_version, utils, AddressScript, DeviceKind, Error as HWIError, CHANGE_INDEX, HWI,
31 RECV_INDEX,
32};
33
34pub use hidapi::{DeviceInfo, HidApi};
35pub use ledger_bitcoin_client::async_client::Transport;
36
37#[derive(Default)]
38struct CommandOptions {
39 wallet: Option<(WalletPolicy, Option<[u8; 32]>)>,
40 display_xpub: bool,
41}
42
43pub struct Ledger<T: Transport> {
44 client: BitcoinClient<T>,
45 options: CommandOptions,
46 kind: DeviceKind,
47}
48
49impl<T: Transport> Ledger<T> {
50 pub fn display_xpub(mut self, display: bool) -> Result<Self, HWIError> {
51 self.options.display_xpub = display;
52 Ok(self)
53 }
54
55 pub fn with_wallet(
56 mut self,
57 name: impl Into<String>,
58 policy: &str,
59 hmac: Option<[u8; 32]>,
60 ) -> Result<Self, HWIError> {
61 let (descriptor_template, keys) = utils::extract_keys_and_template::<WalletPubKey>(policy)?;
62 let wallet = WalletPolicy::new(name.into(), WalletVersion::V2, descriptor_template, keys);
63 self.options.wallet = Some((wallet, hmac));
64 Ok(self)
65 }
66}
67
68impl<T: Transport> std::fmt::Debug for Ledger<T> {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("Ledger").finish()
72 }
73}
74
75impl<T: 'static + Transport + Sync + Send> From<Ledger<T>> for Box<dyn HWI + Send> {
76 fn from(s: Ledger<T>) -> Box<dyn HWI + Send> {
77 Box::new(s)
78 }
79}
80
81#[async_trait]
82impl<T: Transport + Sync + Send> HWI for Ledger<T> {
83 fn device_kind(&self) -> DeviceKind {
84 self.kind
85 }
86
87 async fn get_version(&self) -> Result<super::Version, HWIError> {
88 let (_, version, _) = self.client.get_version().await?;
89 Ok(parse_version(&version)?)
90 }
91
92 async fn get_master_fingerprint(&self) -> Result<Fingerprint, HWIError> {
93 Ok(self.client.get_master_fingerprint().await?)
94 }
95
96 async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result<Xpub, HWIError> {
97 Ok(self
98 .client
99 .get_extended_pubkey(path, self.options.display_xpub)
100 .await?)
101 }
102
103 async fn display_address(&self, script: &AddressScript) -> Result<(), HWIError> {
104 match script {
105 AddressScript::P2TR(path) => {
106 let children = utils::bip86_path_child_numbers(path.clone())?;
107 let (hardened_children, normal_children) = children.split_at(3);
108 let path = DerivationPath::from(hardened_children);
109 let fg = self.get_master_fingerprint().await?;
110 let xpub = self.get_extended_pubkey(&path).await?;
111 let policy = format!("tr({}/**)", key_string_from_parts(fg, path, xpub));
112 let (descriptor_template, keys) =
113 utils::extract_keys_and_template::<WalletPubKey>(&policy)?;
114 let wallet =
115 WalletPolicy::new("".into(), WalletVersion::V2, descriptor_template, keys);
116
117 if ![RECV_INDEX, CHANGE_INDEX].contains(&normal_children[0]) {
118 return Err(HWIError::Bip86ChangeIndex);
119 }
120 self.client
121 .get_wallet_address(
122 &wallet,
123 None,
124 normal_children[0] == CHANGE_INDEX,
125 normal_children[1].into(),
126 true,
127 )
128 .await?;
129 }
130 AddressScript::Miniscript { index, change } => {
131 let (policy, hmac) = &self
132 .options
133 .wallet
134 .as_ref()
135 .ok_or_else(|| HWIError::MissingPolicy)?;
136 self.client
137 .get_wallet_address(policy, hmac.as_ref(), *change, *index, true)
138 .await?;
139 }
140 }
141 Ok(())
142 }
143
144 async fn register_wallet(
145 &self,
146 name: &str,
147 policy: &str,
148 ) -> Result<Option<[u8; 32]>, HWIError> {
149 let (descriptor_template, keys) = utils::extract_keys_and_template::<WalletPubKey>(policy)?;
150 let wallet = WalletPolicy::new(
151 name.to_string(),
152 WalletVersion::V2,
153 descriptor_template,
154 keys,
155 );
156 let (_id, hmac) = self.client.register_wallet(&wallet).await?;
157 Ok(Some(hmac))
158 }
159
160 async fn is_wallet_registered(&self, name: &str, policy: &str) -> Result<bool, HWIError> {
161 if let Some((wallet, hmac)) = &self.options.wallet {
162 let (descriptor_template, keys) =
163 utils::extract_keys_and_template::<WalletPubKey>(policy)?;
164 Ok(hmac.is_some()
165 && name == wallet.name
166 && descriptor_template == wallet.descriptor_template
167 && keys == wallet.keys)
168 } else {
169 Ok(false)
170 }
171 }
172
173 async fn sign_tx(&self, psbt: &mut Psbt) -> Result<(), HWIError> {
174 if let Some((policy, hmac)) = &self.options.wallet {
175 let sigs = self.client.sign_psbt(psbt, policy, hmac.as_ref()).await?;
176 for (i, sig) in sigs {
177 let input = psbt.inputs.get_mut(i).ok_or(HWIError::DeviceDidNotSign)?;
178 match sig {
179 PartialSignature::Sig(key, sig) => {
180 input.partial_sigs.insert(key, sig);
181 }
182 PartialSignature::TapScriptSig(key, Some(tapleaf_hash), sig) => {
183 input.tap_script_sigs.insert((key, tapleaf_hash), sig);
184 }
185 PartialSignature::TapScriptSig(_, None, sig) => {
186 input.tap_key_sig = Some(sig);
187 }
188 }
189 }
190 Ok(())
191 } else {
192 Err(HWIError::UnimplementedMethod)
194 }
195 }
196}
197
198fn key_string_from_parts(fg: Fingerprint, path: DerivationPath, xpub: Xpub) -> String {
199 format!(
200 "[{}/{}]{}",
201 fg,
202 path.to_string().trim_start_matches("m/"),
203 xpub
204 )
205}
206
207impl Ledger<TransportHID> {
208 pub fn enumerate(api: &HidApi) -> impl Iterator<Item = &DeviceInfo> {
209 TransportNativeHID::list_ledgers(api)
210 }
211
212 pub fn connect(api: &HidApi, device: &DeviceInfo) -> Result<Self, HWIError> {
213 let hid =
214 TransportNativeHID::open_device(api, device).map_err(|_| HWIError::DeviceNotFound)?;
215 Ok(Ledger {
216 client: BitcoinClient::new(TransportHID(hid)),
217 options: CommandOptions::default(),
218 kind: DeviceKind::Ledger,
219 })
220 }
221
222 pub fn try_connect_hid() -> Result<Self, HWIError> {
223 let hid = TransportNativeHID::new(&HidApi::new().map_err(|_| HWIError::DeviceNotFound)?)
224 .map_err(|_| HWIError::DeviceNotFound)?;
225 Ok(Ledger {
226 client: BitcoinClient::new(TransportHID(hid)),
227 options: CommandOptions::default(),
228 kind: DeviceKind::Ledger,
229 })
230 }
231}
232
233pub struct TransportHID(TransportNativeHID);
235
236#[async_trait]
237impl Transport for TransportHID {
238 type Error = Box<dyn Error>;
239 async fn exchange(&self, cmd: &APDUCommand) -> Result<(StatusWord, Vec<u8>), Self::Error> {
240 self.0
241 .exchange(&ledger_apdu::APDUCommand {
242 ins: cmd.ins,
243 cla: cmd.cla,
244 p1: cmd.p1,
245 p2: cmd.p2,
246 data: cmd.data.clone(),
247 })
248 .map(|answer| {
249 (
250 StatusWord::try_from(answer.retcode()).unwrap_or(StatusWord::Unknown),
251 answer.data().to_vec(),
252 )
253 })
254 .map_err(|e| e.into())
255 }
256}
257
258pub type LedgerSimulator = Ledger<TransportTcp>;
259
260impl LedgerSimulator {
261 pub async fn try_connect() -> Result<Self, HWIError> {
262 let transport = TransportTcp::new()
263 .await
264 .map_err(|_| HWIError::DeviceNotFound)?;
265 Ok(Ledger {
266 client: BitcoinClient::new(transport),
267 options: CommandOptions::default(),
268 kind: DeviceKind::LedgerSimulator,
269 })
270 }
271}
272
273pub struct TransportTcp {
275 connection: Mutex<TcpStream>,
276}
277
278impl TransportTcp {
279 pub async fn new() -> Result<Self, Box<dyn Error>> {
280 let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9999);
281 let stream = TcpStream::connect(addr).await?;
282 Ok(Self {
283 connection: Mutex::new(stream),
284 })
285 }
286}
287
288#[async_trait]
289impl Transport for TransportTcp {
290 type Error = Box<dyn Error>;
291 async fn exchange(&self, command: &APDUCommand) -> Result<(StatusWord, Vec<u8>), Self::Error> {
292 let mut stream = self.connection.lock().await;
293 let command_bytes = command.encode();
294
295 let mut req = vec![0u8; command_bytes.len() + 4];
296 req[..4].copy_from_slice(&(command_bytes.len() as u32).to_be_bytes());
297 req[4..].copy_from_slice(&command_bytes);
298 stream.write_all(&req).await?;
299
300 let mut buff = [0u8; 4];
301 let len = match stream.read(&mut buff).await? {
302 4 => u32::from_be_bytes(buff),
303 _ => return Err("Invalid Length".into()),
304 };
305
306 let mut resp = vec![0u8; len as usize + 2];
307 stream.read_exact(&mut resp).await?;
308 let answer = APDUAnswer::from_answer(resp).map_err(|_| "Invalid Answer")?;
309 Ok((
310 StatusWord::try_from(answer.retcode()).unwrap_or(StatusWord::Unknown),
311 answer.data().to_vec(),
312 ))
313 }
314}
315
316impl<T: core::fmt::Debug> From<BitcoinClientError<T>> for HWIError {
317 fn from(e: BitcoinClientError<T>) -> HWIError {
318 if let BitcoinClientError::Device { status, .. } = e {
319 if status == StatusWord::Deny {
320 return HWIError::UserRefused;
321 }
322 };
323 HWIError::Device(format!("{:#?}", e))
324 }
325}
326
327#[cfg(test)]
328mod tests {
329
330 use super::*;
331 use std::str::FromStr;
332
333 #[test]
335 fn test_key_string_from_parts() {
336 let path = DerivationPath::from_str("m/48'/1'/0'/2'").unwrap();
337 let fg = Fingerprint::from_hex("aabbccdd").unwrap();
338 let xpub = Xpub::from_str("tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N").unwrap();
339 assert_eq!(key_string_from_parts(fg, path, xpub), "[aabbccdd/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N");
340 }
341}