1use 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; const INS_GET_PUBLIC_KEY: u8 = 4; const INS_GET_WALLET_ID: u8 = 0x05; const INS_GET_VERSION: u8 = 6; const INS_SIGN_TRANSACTION: u8 = 2; const INS_SIGN_NEP413_MESSAGE: u8 = 7; const INS_SIGN_NEP366_DELEGATE_ACTION: u8 = 8; const NETWORK_ID: u8 = 'W' as u8; const RETURN_CODE_OK: u16 = 36864; const CHUNK_SIZE: usize = 250; pub 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
34pub type UNCLedgerAppVersion = Vec<u8>;
36pub type SignatureBytes = Vec<u8>;
38
39#[derive(Debug)]
40pub enum UNCLedgerError {
41 HidApiError(HidError),
43 LedgerHidError(LedgerHIDError),
45 APDUExchangeError(String),
47 LedgerHIDError(LedgerHIDError),
49}
50
51fn 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
75pub fn get_version() -> Result<UNCLedgerAppVersion, UNCLedgerError> {
82 let transport = get_transport()?;
86 let command = APDUCommand {
87 cla: CLA,
88 ins: INS_GET_VERSION,
89 p1: 0, 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 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
119pub 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 let transport = get_transport()?;
170
171 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, 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 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 let transport = get_transport()?;
218
219 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, 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 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 let hidapi = HidApi::new().map_err(UNCLedgerError::HidApiError)?;
258 TransportNativeHID::new(&hidapi).map_err(UNCLedgerError::LedgerHidError)
259}
260
261pub fn sign_transaction(
300 unsigned_tx: BorshSerializedUnsignedTransaction,
301 seed_phrase_hd_path: slip10::BIP32Path,
302) -> Result<SignatureBytes, UNCLedgerError> {
303 let transport = get_transport()?;
304 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 }, 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 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 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 }, 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 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 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 }, 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 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}