unc_ledger/
lib.rs

1//! UNC <-> Ledger transport
2//!
3//! Provides a set of commands that can be executed to communicate with UNC App installed on Ledger device:
4//! - Read PublicKey from Ledger device by HD Path
5//! - Sign a Transaction
6use ledger_transport::APDUCommand;
7use ledger_transport_hid::{
8    hidapi::{HidApi, HidError},
9    LedgerHIDError, TransportNativeHID,
10};
11use unc_primitives::action::delegate::DelegateAction;
12use unc_primitives_core::borsh::{self, BorshSerialize};
13
14const CLA: u8 = 0x80; // Instruction class
15const INS_GET_PUBLIC_KEY: u8 = 4; // Instruction code to get public key
16const INS_GET_WALLET_ID: u8 = 0x05; // Get Wallet ID
17const INS_GET_VERSION: u8 = 6; // Instruction code to get app version from the Ledger
18const INS_SIGN_TRANSACTION: u8 = 2; // Instruction code to sign a transaction on the Ledger
19const INS_SIGN_NEP413_MESSAGE: u8 = 7; // Instruction code to sign a nep-413 message with Ledger
20const INS_SIGN_NEP366_DELEGATE_ACTION: u8 = 8; // Instruction code to sign a nep-413 message with Ledger
21const NETWORK_ID: u8 = 'W' as u8; // Instruction parameter 2
22const RETURN_CODE_OK: u16 = 36864; // APDUAnswer.retcode which means success from Ledger
23const CHUNK_SIZE: usize = 250; // Chunk size to be sent to Ledger
24
25/// Alias of `Vec<u8>`. The goal is naming to help understand what the bytes to deal with
26pub type BorshSerializedUnsignedTransaction = Vec<u8>;
27
28const P1_GET_PUB_DISPLAY: u8 = 0;
29const P1_GET_PUB_SILENT: u8 = 1;
30
31const P1_SIGN_NORMAL: u8 = 0;
32const P1_SIGN_NORMAL_LAST_CHUNK: u8 = 0x80;
33
34/// Alias of `Vec<u8>`. The goal is naming to help understand what the bytes to deal with
35pub type UNCLedgerAppVersion = Vec<u8>;
36/// Alias of `Vec<u8>`. The goal is naming to help understand what the bytes to deal with
37pub type SignatureBytes = Vec<u8>;
38
39#[derive(Debug)]
40pub enum UNCLedgerError {
41    /// Error occuring on init of hidapid and getting current devices list
42    HidApiError(HidError),
43    /// Error occuring on creating a new hid transport, connecting to first ledger device found  
44    LedgerHidError(LedgerHIDError),
45    /// Error occurred while exchanging with Ledger device
46    APDUExchangeError(String),
47    /// Error with transport
48    LedgerHIDError(LedgerHIDError),
49}
50
51/// Converts BIP32Path into bytes (`Vec<u8>`)
52fn hd_path_to_bytes(hd_path: &slip10::BIP32Path) -> Vec<u8> {
53    (0..hd_path.depth())
54        .map(|index| {
55            let value = *hd_path.index(index).unwrap();
56            value.to_be_bytes()
57        })
58        .flatten()
59        .collect::<Vec<u8>>()
60}
61
62#[inline(always)]
63fn log_command(index: usize, is_last_chunk: bool, command: &APDUCommand<Vec<u8>>) {
64    log::info!(
65        "APDU  in{}: {}",
66        if is_last_chunk {
67            " (last)".to_string()
68        } else {
69            format!(" ({})", index)
70        },
71        hex::encode(&command.serialize())
72    );
73}
74
75/// Get the version of UNC App installed on Ledger
76///
77/// # Returns
78///
79/// * A `Result` whose `Ok` value is an `UNCLedgerAppVersion` (just a `Vec<u8>` for now, where first value is a major version, second is a minor and the last is the path)
80///  and whose `Err` value is a `UNCLedgerError` containing an error which occurred.
81pub fn get_version() -> Result<UNCLedgerAppVersion, UNCLedgerError> {
82    //! Something
83    // instantiate the connection to Ledger
84    // will return an error if Ledger is not connected
85    let transport = get_transport()?;
86    let command = APDUCommand {
87        cla: CLA,
88        ins: INS_GET_VERSION,
89        p1: 0, // Instruction parameter 1 (offset)
90        p2: 0,
91        data: vec![],
92    };
93
94    log::info!("APDU  in: {}", hex::encode(&command.serialize()));
95
96    match transport.exchange(&command) {
97        Ok(response) => {
98            log::info!(
99                "APDU out: {}\nAPDU ret code: {:x}",
100                hex::encode(response.apdu_data()),
101                response.retcode(),
102            );
103            // Ok means we successfully exchanged with the Ledger
104            // but doesn't mean our request succeeded
105            // we need to check it based on `response.retcode`
106            if response.retcode() == RETURN_CODE_OK {
107                return Ok(response.data().to_vec());
108            } else {
109                let retcode = response.retcode();
110
111                let error_string = format!("Ledger APDU retcode: 0x{:X}", retcode);
112                return Err(UNCLedgerError::APDUExchangeError(error_string));
113            }
114        }
115        Err(err) => return Err(UNCLedgerError::LedgerHIDError(err)),
116    };
117}
118
119/// Gets PublicKey from the Ledger on the given `hd_path`
120///
121/// # Inputs
122/// * `hd_path` - seed phrase hd path `slip10::BIP32Path` for which PublicKey to look
123///
124/// # Returns
125///
126/// * A `Result` whose `Ok` value is an `ed25519_dalek::PublicKey` and whose `Err` value is a
127///   `UNCLedgerError` containing an error which
128///   occurred.
129///
130/// # Examples
131///
132/// ```no_run
133/// use unc_ledger::get_public_key;
134/// use slip10::BIP32Path;
135/// use std::str::FromStr;
136///
137/// # fn main() {
138/// let hd_path = BIP32Path::from_str("44'/397'/0'/0'/1'").unwrap();
139/// let public_key = get_public_key(hd_path).unwrap();
140/// println!("{:#?}", public_key);
141/// # }
142/// ```
143///
144/// # Trick
145///
146/// To convert the answer into `unc_crypto::PublicKey` do:
147///
148/// ```
149/// # let public_key_bytes = [10u8; 32];
150/// # let public_key = ed25519_dalek::PublicKey::from_bytes(&public_key_bytes).unwrap();
151/// let public_key = unc_crypto::PublicKey::ED25519(
152///     unc_crypto::ED25519PublicKey::from(
153///         public_key.to_bytes(),
154///     )
155/// );
156/// ```
157pub fn get_public_key(
158    hd_path: slip10::BIP32Path,
159) -> Result<ed25519_dalek::PublicKey, UNCLedgerError> {
160    get_public_key_with_display_flag(hd_path, true)
161}
162
163pub fn get_public_key_with_display_flag(
164    hd_path: slip10::BIP32Path,
165    display_and_confirm: bool,
166) -> Result<ed25519_dalek::PublicKey, UNCLedgerError> {
167    // instantiate the connection to Ledger
168    // will return an error if Ledger is not connected
169    let transport = get_transport()?;
170
171    // hd_path must be converted into bytes to be sent as `data` to the Ledger
172    let hd_path_bytes = hd_path_to_bytes(&hd_path);
173
174    let p1 = if display_and_confirm {
175        P1_GET_PUB_DISPLAY
176    } else {
177        P1_GET_PUB_SILENT
178    };
179
180    let command = APDUCommand {
181        cla: CLA,
182        ins: INS_GET_PUBLIC_KEY,
183        p1, // Instruction parameter 1 (offset)
184        p2: NETWORK_ID,
185        data: hd_path_bytes,
186    };
187    log::info!("APDU  in: {}", hex::encode(&command.serialize()));
188
189    match transport.exchange(&command) {
190        Ok(response) => {
191            log::info!(
192                "APDU out: {}\nAPDU ret code: {:x}",
193                hex::encode(response.apdu_data()),
194                response.retcode(),
195            );
196            // Ok means we successfully exchanged with the Ledger
197            // but doesn't mean our request succeeded
198            // we need to check it based on `response.retcode`
199            if response.retcode() == RETURN_CODE_OK {
200                return Ok(ed25519_dalek::PublicKey::from_bytes(&response.data()).unwrap());
201            } else {
202                let retcode = response.retcode();
203
204                let error_string = format!("Ledger APDU retcode: 0x{:X}", retcode);
205                return Err(UNCLedgerError::APDUExchangeError(error_string));
206            }
207        }
208        Err(err) => return Err(UNCLedgerError::LedgerHIDError(err)),
209    };
210}
211
212pub fn get_wallet_id(
213    hd_path: slip10::BIP32Path,
214) -> Result<ed25519_dalek::PublicKey, UNCLedgerError> {
215    // instantiate the connection to Ledger
216    // will return an error if Ledger is not connected
217    let transport = get_transport()?;
218
219    // hd_path must be converted into bytes to be sent as `data` to the Ledger
220    let hd_path_bytes = hd_path_to_bytes(&hd_path);
221
222    let command = APDUCommand {
223        cla: CLA,
224        ins: INS_GET_WALLET_ID,
225        p1: 0, // Instruction parameter 1 (offset)
226        p2: NETWORK_ID,
227        data: hd_path_bytes,
228    };
229    log::info!("APDU  in: {}", hex::encode(&command.serialize()));
230
231    match transport.exchange(&command) {
232        Ok(response) => {
233            log::info!(
234                "APDU out: {}\nAPDU ret code: {:x}",
235                hex::encode(response.apdu_data()),
236                response.retcode(),
237            );
238            // Ok means we successfully exchanged with the Ledger
239            // but doesn't mean our request succeeded
240            // we need to check it based on `response.retcode`
241            if response.retcode() == RETURN_CODE_OK {
242                return Ok(ed25519_dalek::PublicKey::from_bytes(&response.data()).unwrap());
243            } else {
244                let retcode = response.retcode();
245
246                let error_string = format!("Ledger APDU retcode: 0x{:X}", retcode);
247                return Err(UNCLedgerError::APDUExchangeError(error_string));
248            }
249        }
250        Err(err) => return Err(UNCLedgerError::LedgerHIDError(err)),
251    };
252}
253
254fn get_transport() -> Result<TransportNativeHID, UNCLedgerError> {
255    // instantiate the connection to Ledger
256    // will return an error if Ledger is not connected
257    let hidapi = HidApi::new().map_err(UNCLedgerError::HidApiError)?;
258    TransportNativeHID::new(&hidapi).map_err(UNCLedgerError::LedgerHidError)
259}
260
261/// Sign the transaction. Transaction should be [borsh serialized](https://github.com/unc/borsh-rs) `Vec<u8>`
262///
263/// # Inputs
264/// * `unsigned_transaction_borsh_serializer` - unsigned transaction `unc_primitives::transaction::Transaction`
265/// which is serialized with `BorshSerializer` and basically is just `Vec<u8>`
266/// * `seed_phrase_hd_path` - seed phrase hd path `slip10::BIP32Path` with which to sign
267///
268/// # Returns
269///
270/// * A `Result` whose `Ok` value is an `Signature` (bytes) and whose `Err` value is a
271/// `UNCLedgerError` containing an error which occurred.
272///
273/// # Examples
274///
275/// ```no_run
276/// use unc_ledger::sign_transaction;
277/// use unc_primitives::{borsh, borsh::BorshSerialize};
278/// use slip10::BIP32Path;
279/// use std::str::FromStr;
280///
281/// # fn main() {
282/// # let unc_unsigned_transaction = [10; 250];
283/// let hd_path = BIP32Path::from_str("44'/397'/0'/0'/1'").unwrap();
284/// let borsh_transaction = borsh::to_vec(&unc_unsigned_transaction).unwrap();
285/// let signature = sign_transaction(borsh_transaction, hd_path).unwrap();
286/// println!("{:#?}", signature);
287/// # }
288/// ```
289///
290/// # Trick
291///
292/// To convert the answer into `unc_crypto::Signature` do:
293///
294/// ```
295/// # let signature = [10; 64].to_vec();
296/// let signature = unc_crypto::Signature::from_parts(unc_crypto::KeyType::ED25519, &signature)
297///     .expect("Signature is not expected to fail on deserialization");
298/// ```
299pub fn sign_transaction(
300    unsigned_tx: BorshSerializedUnsignedTransaction,
301    seed_phrase_hd_path: slip10::BIP32Path,
302) -> Result<SignatureBytes, UNCLedgerError> {
303    let transport = get_transport()?;
304    // seed_phrase_hd_path must be converted into bytes to be sent as `data` to the Ledger
305    let hd_path_bytes = hd_path_to_bytes(&seed_phrase_hd_path);
306
307    let mut data: Vec<u8> = vec![];
308    data.extend(hd_path_bytes);
309    data.extend(&unsigned_tx);
310
311    let chunks = data.chunks(CHUNK_SIZE);
312    let chunks_count = chunks.len();
313
314    for (i, chunk) in chunks.enumerate() {
315        let is_last_chunk = chunks_count == i + 1;
316        let command = APDUCommand {
317            cla: CLA,
318            ins: INS_SIGN_TRANSACTION,
319            p1: if is_last_chunk {
320                P1_SIGN_NORMAL_LAST_CHUNK
321            } else {
322                P1_SIGN_NORMAL
323            }, // Instruction parameter 1 (offset)
324            p2: NETWORK_ID,
325            data: chunk.to_vec(),
326        };
327        log_command(i, is_last_chunk, &command);
328        match transport.exchange(&command) {
329            Ok(response) => {
330                log::info!(
331                    "APDU out: {}\nAPDU ret code: {:x}",
332                    hex::encode(response.apdu_data()),
333                    response.retcode(),
334                );
335                // Ok means we successfully exchanged with the Ledger
336                // but doesn't mean our request succeeded
337                // we need to check it based on `response.retcode`
338                if response.retcode() == RETURN_CODE_OK {
339                    if is_last_chunk {
340                        return Ok(response.data().to_vec());
341                    }
342                } else {
343                    let retcode = response.retcode();
344
345                    let error_string = format!("Ledger APDU retcode: 0x{:X}", retcode);
346                    return Err(UNCLedgerError::APDUExchangeError(error_string));
347                }
348            }
349            Err(err) => return Err(UNCLedgerError::LedgerHIDError(err)),
350        };
351    }
352    Err(UNCLedgerError::APDUExchangeError(
353        "Unable to process request".to_owned(),
354    ))
355}
356
357#[derive(Debug, BorshSerialize)]
358#[borsh(crate = "unc_primitives_core::borsh")]
359pub struct NEP413Payload {
360    pub messsage: String,
361    pub nonce: [u8; 32],
362    pub recipient: String,
363    pub callback_url: Option<String>,
364}
365
366pub fn sign_message_nep413(
367    payload: &NEP413Payload,
368    seed_phrase_hd_path: slip10::BIP32Path,
369) -> Result<SignatureBytes, UNCLedgerError> {
370    let transport = get_transport()?;
371    // seed_phrase_hd_path must be converted into bytes to be sent as `data` to the Ledger
372    let hd_path_bytes = hd_path_to_bytes(&seed_phrase_hd_path);
373
374    let mut data: Vec<u8> = vec![];
375    data.extend(hd_path_bytes);
376    data.extend_from_slice(&borsh::to_vec(payload).unwrap());
377
378    let chunks = data.chunks(CHUNK_SIZE);
379    let chunks_count = chunks.len();
380
381    for (i, chunk) in chunks.enumerate() {
382        let is_last_chunk = chunks_count == i + 1;
383        let command = APDUCommand {
384            cla: CLA,
385            ins: INS_SIGN_NEP413_MESSAGE,
386            p1: if is_last_chunk {
387                P1_SIGN_NORMAL_LAST_CHUNK
388            } else {
389                P1_SIGN_NORMAL
390            }, // Instruction parameter 1 (offset)
391            p2: NETWORK_ID,
392            data: chunk.to_vec(),
393        };
394        log_command(i, is_last_chunk, &command);
395        match transport.exchange(&command) {
396            Ok(response) => {
397                log::info!(
398                    "APDU out: {}\nAPDU ret code: {:x}",
399                    hex::encode(response.apdu_data()),
400                    response.retcode(),
401                );
402                // Ok means we successfully exchanged with the Ledger
403                // but doesn't mean our request succeeded
404                // we need to check it based on `response.retcode`
405                if response.retcode() == RETURN_CODE_OK {
406                    if is_last_chunk {
407                        return Ok(response.data().to_vec());
408                    }
409                } else {
410                    let retcode = response.retcode();
411
412                    let error_string = format!("Ledger APDU retcode: 0x{:X}", retcode);
413                    return Err(UNCLedgerError::APDUExchangeError(error_string));
414                }
415            }
416            Err(err) => return Err(UNCLedgerError::LedgerHIDError(err)),
417        };
418    }
419    Err(UNCLedgerError::APDUExchangeError(
420        "Unable to process request".to_owned(),
421    ))
422}
423
424pub fn sign_message_nep366_delegate_action(
425    payload: &DelegateAction,
426    seed_phrase_hd_path: slip10::BIP32Path,
427) -> Result<SignatureBytes, UNCLedgerError> {
428    let transport = get_transport()?;
429    // seed_phrase_hd_path must be converted into bytes to be sent as `data` to the Ledger
430    let hd_path_bytes = hd_path_to_bytes(&seed_phrase_hd_path);
431
432    let mut data: Vec<u8> = vec![];
433    data.extend(hd_path_bytes);
434    data.extend_from_slice(&borsh::to_vec(payload).unwrap());
435
436    let chunks = data.chunks(CHUNK_SIZE);
437    let chunks_count = chunks.len();
438
439    for (i, chunk) in chunks.enumerate() {
440        let is_last_chunk = chunks_count == i + 1;
441        let command = APDUCommand {
442            cla: CLA,
443            ins: INS_SIGN_NEP366_DELEGATE_ACTION,
444            p1: if is_last_chunk {
445                P1_SIGN_NORMAL_LAST_CHUNK
446            } else {
447                P1_SIGN_NORMAL
448            }, // Instruction parameter 1 (offset)
449            p2: NETWORK_ID,
450            data: chunk.to_vec(),
451        };
452        log_command(i, is_last_chunk, &command);
453        match transport.exchange(&command) {
454            Ok(response) => {
455                log::info!(
456                    "APDU out: {}\nAPDU ret code: {:x}",
457                    hex::encode(response.apdu_data()),
458                    response.retcode(),
459                );
460                // Ok means we successfully exchanged with the Ledger
461                // but doesn't mean our request succeeded
462                // we need to check it based on `response.retcode`
463                if response.retcode() == RETURN_CODE_OK {
464                    if is_last_chunk {
465                        return Ok(response.data().to_vec());
466                    }
467                } else {
468                    let retcode = response.retcode();
469
470                    let error_string = format!("Ledger APDU retcode: 0x{:X}", retcode);
471                    return Err(UNCLedgerError::APDUExchangeError(error_string));
472                }
473            }
474            Err(err) => return Err(UNCLedgerError::LedgerHIDError(err)),
475        };
476    }
477    Err(UNCLedgerError::APDUExchangeError(
478        "Unable to process request".to_owned(),
479    ))
480}