Skip to main content

emerald_hwkey/ledger/app/
ethereum.rs

1use std::str::from_utf8;
2use std::sync::{Arc, Mutex};
3use crate::{
4    errors::HWKeyError,
5    ledger::{
6        apdu::ApduBuilder,
7        app::{AsChainCode, AsExtendedKey, AsPubkey, LedgerApp, PubkeyAddressApp},
8        comm::{sendrecv, LedgerTransport},
9        commons::as_compact
10    },
11};
12use std::convert::TryFrom;
13use hdpath::{AccountHDPath, HDPath, Purpose};
14use bitcoin::{
15    secp256k1::PublicKey,
16    bip32::ChainCode
17};
18use crate::ledger::connect::direct::CHUNK_SIZE;
19
20/// ECDSA crypto signature length in bytes
21pub const ECDSA_SIGNATURE_BYTES: usize = 65;
22
23const COMMAND_GET_ADDRESS: u8 = 0x02;
24const COMMAND_SIGN_TRANSACTION: u8 = 0x04;
25const COMMAND_SIGN_MESSAGE: u8 = 0x08;
26const COMMAND_APP_CONFIG: u8 = 0x06;
27const COMMAND_SIGN_EIP712: u8 = 0x0C;
28
29/// The signature as it's used in Ethereum
30/// R-S-V as 65 bytes
31/// i.e., 32 bytes for R, 32 bytes for S, and 1 byte for V
32///
33#[derive(Clone, Eq, PartialEq, Hash)]
34pub struct SignatureBytes([u8; ECDSA_SIGNATURE_BYTES]);
35
36impl SignatureBytes {
37
38    /// Ledger answers with V-R-S, but Ethereum uses R-S-V, so here we move that byte to the end (and move others to the left)
39    pub fn from_ledger_bytes(bytes: &[u8]) -> Result<Self, HWKeyError> {
40        if bytes.len() != ECDSA_SIGNATURE_BYTES {
41            return Err(HWKeyError::CryptoError(format!(
42                "Invalid signature length. Expected: {}, received: {}",
43                ECDSA_SIGNATURE_BYTES, bytes.len()
44            )));
45        }
46        let mut val: [u8; ECDSA_SIGNATURE_BYTES] = [0; ECDSA_SIGNATURE_BYTES];
47        val[0..64].copy_from_slice(&bytes[1..65]);
48        val[64] = bytes[0];
49        Ok(SignatureBytes(val))
50    }
51
52    pub fn to_vec(&self) -> Vec<u8> {
53        self.0.to_vec()
54    }
55
56    /// Convert to the original V-R-S format as it was received from Ledger
57    pub fn to_ledger_bytes(&self) -> [u8; ECDSA_SIGNATURE_BYTES] {
58        let mut vrs = [0; ECDSA_SIGNATURE_BYTES];
59        vrs[1..65].copy_from_slice(&self.0[0..64]);
60        vrs[0] = self.0[64];
61        vrs
62    }
63}
64
65impl std::fmt::Debug for SignatureBytes {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        write!(f, "0x{}", hex::encode(self.0))
68    }
69}
70
71impl AsRef<[u8]> for SignatureBytes {
72    fn as_ref(&self) -> &[u8] {
73        &self.0
74    }
75}
76
77impl AsRef<[u8; ECDSA_SIGNATURE_BYTES]> for SignatureBytes {
78    fn as_ref(&self) -> &[u8; ECDSA_SIGNATURE_BYTES] {
79        &self.0
80    }
81}
82
83pub struct EthereumApp {
84    ledger: Arc<Mutex<dyn LedgerTransport>>
85}
86
87#[derive(Debug, Clone, Eq, PartialEq)]
88pub struct AddressResponse {
89    pub pubkey: PublicKey,
90    pub address: String,
91    pub chaincode: ChainCode
92}
93
94impl AsPubkey for AddressResponse {
95    fn as_pubkey(&self) -> &PublicKey {
96        &self.pubkey
97    }
98}
99
100impl AsChainCode for AddressResponse {
101    fn as_chaincode(&self) -> &ChainCode {
102        &self.chaincode
103    }
104}
105
106impl AsExtendedKey for AddressResponse {}
107
108impl TryFrom<Vec<u8>> for AddressResponse {
109    type Error = HWKeyError;
110
111    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
112        if value.is_empty() {
113            return Err(HWKeyError::EncodingError("Empty data".to_string()))
114        }
115        let pubkey_len = value[0] as usize;
116        if 1 + pubkey_len > value.len() {
117            return Err(HWKeyError::EncodingError(
118                format!("Pubkey cutoff. {:?} > {:?} for {:?}", 1 + pubkey_len, value.len(), pubkey_len)
119            ))
120        }
121        let pubkey = &value[1..pubkey_len+1];
122        let pubkey = PublicKey::from_slice(pubkey)
123            .map_err(|_| HWKeyError::CryptoError("Invalid public key".to_string()))?;
124        let pubkey_comp = as_compact(&pubkey)?;
125
126        let address_len = value[pubkey_len + 1] as usize;
127        let address_start = 1 + pubkey_len + 1;
128        let address_end = address_start + address_len;
129        if address_end > value.len() {
130            return Err(HWKeyError::EncodingError(
131                format!("Address cutoff. {:?} > {:?} as {:?}..{:?} for {:?}", address_end, value.len(), address_start, address_end, address_len)
132            ))
133        }
134        let address = &value[address_start..address_end];
135        let address = from_utf8(address)
136            .map(|a| a.to_string())
137            .map(|a| if a.starts_with("0x") { a } else { format!("0x{}", a)} )
138            .map_err(|e| HWKeyError::EncodingError(format!("Can't parse address: {}", e)))?;
139
140        let chaincode_len = 32_usize;
141        let chaincode_start = address_end;
142        let chaincode_end = chaincode_start + chaincode_len;
143        if chaincode_end > value.len() {
144            return Err(HWKeyError::EncodingError(
145                format!("Chaincode cutoff. {:?} > {:?}", chaincode_end, value.len())
146            ))
147        }
148        let chaincode = value[chaincode_start..chaincode_end].to_vec();
149        let chaincode = ChainCode::try_from(chaincode.as_slice()).unwrap();
150
151        Ok(AddressResponse {
152            pubkey: pubkey_comp, address, chaincode
153        })
154    }
155}
156
157#[derive(Debug, Clone, Eq, PartialEq)]
158pub struct AppVersion {
159    ///  arbitrary data signature enabled by user
160    pub data_sign_enabled: bool,
161    /// ERC 20 Token information needs to be provided externally
162    pub external_erc20: bool,
163    pub version_major: u8,
164    pub version_minor: u8,
165    pub version_patch: u8,
166}
167
168impl TryFrom<Vec<u8>> for AppVersion {
169    type Error = ();
170
171    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
172        if value.len() < 4 {
173            return Err(())
174        }
175        let flags = value[0];
176        Ok(AppVersion {
177            data_sign_enabled: flags & 0x01 > 0,
178            external_erc20: flags & 0x02 > 0,
179            version_major: value[1],
180            version_minor: value[2],
181            version_patch: value[3]
182        })
183    }
184}
185
186impl EthereumApp {
187
188    /// Split data into chunks for sending to Ledger device
189    /// 
190    /// # Arguments
191    /// * `data` - The data to be chunked
192    /// * `hd_path` - HD path that will be included in the initial chunk
193    /// 
194    /// # Returns
195    /// A tuple of (initial_chunk, continuation_data) where:
196    /// * initial_chunk - First chunk including HD path data
197    /// * continuation_data - Remaining data to be sent in subsequent chunks
198    fn chunk_data<'a>(&self, data: &'a [u8], hd_path: &dyn HDPath) -> (&'a [u8], &'a [u8]) {
199        match data.len() {
200            0..=CHUNK_SIZE => (data, &[]),
201            _ => data.split_at(CHUNK_SIZE - hd_path.to_bytes().len()),
202        }
203    }
204
205    /// Send chunked data to the Ledger device using the standard chunking protocol
206    ///
207    /// # Protocol Details:
208    ///
209    /// ```no_run,ignore
210    ///   P1: 00 : first message data block
211    ///       80 : subsequent message data block
212    ///   P2: 00 : always
213    /// ```
214    /// 
215    /// # Arguments
216    /// * `command` - The APDU command to use
217    /// * `hd_path` - HD path for the initial chunk
218    /// * `data` - The data to be sent in chunks
219    /// 
220    /// # Returns
221    /// The response from the device after sending all chunks
222    fn send_chunked_data(&self, command: u8, hd_path: &dyn HDPath, data: &[u8]) -> Result<Vec<u8>, HWKeyError> {
223        let (init, cont) = self.chunk_data(data, hd_path);
224
225        let init_apdu = ApduBuilder::new(command)
226            .with_p1(0x00)
227            .with_data(hd_path.to_bytes().as_slice())
228            .with_data(init)
229            .build();
230
231        let handle = self.ledger.lock().unwrap();
232        let mut res = sendrecv(&*handle, &init_apdu)?;
233
234        for chunk in cont.chunks(CHUNK_SIZE) {
235            let apdu_cont = ApduBuilder::new(command)
236                .with_p1(0x80)
237                .with_data(chunk)
238                .build();
239            res = sendrecv(&*handle, &apdu_cont)?;
240        }
241
242        Ok(res)
243    }
244
245    /// Get address
246    ///
247    /// # Arguments:
248    /// hd_path - HD path, prefixed with count of derivation indexes
249    ///
250    pub fn get_address(&self, hd_path: &dyn HDPath, confirm: bool) -> Result<AddressResponse, HWKeyError> {
251        let apdu = ApduBuilder::new(COMMAND_GET_ADDRESS)
252            // 00 : return address
253            // 01 : display address and confirm before returning
254            .with_p1(if confirm {0x01} else {0x00})
255            // 01 : return the chain code
256            .with_p2(0x01)
257            .with_data(hd_path.to_bytes().as_slice())
258            .build();
259
260        let ledger = self.ledger.lock().unwrap();
261
262
263        // let mut handle = self.ledger.lock().unwrap().deref();
264        sendrecv(&*ledger, &apdu)
265            .and_then(AddressResponse::try_from)
266    }
267
268    /// Sign transaction
269    ///
270    /// # Arguments:
271    /// tx - RLP encoded transaction
272    /// hd_path - HD path, prefixed with count of derivation indexes
273    ///
274    /// # Protocol Details:
275    /// According to the Ledger Ethereum specification (reference/ledger-ethereum.adoc), 
276    /// transaction data is sent as:
277    /// - First block: HD path + RLP transaction chunk (no length prefix)
278    /// - Other blocks: RLP transaction chunk only
279    ///   The RLP transaction data is sent directly without any length prefix.
280    ///
281    pub fn sign_transaction(
282        &self,
283        tx: &[u8],
284        hd_path: &dyn HDPath,
285    ) -> Result<SignatureBytes, HWKeyError> {
286
287        // Send RLP transaction data directly without length prefix
288        // as per SIGN ETH TRANSACTION specification
289        let res = self.send_chunked_data(COMMAND_SIGN_TRANSACTION, hd_path, tx)?;
290        let signature = SignatureBytes::from_ledger_bytes(res.as_slice())?;
291        debug!("Received signature: {:?}", signature);
292        Ok(signature)
293    }
294
295    /// Sign a message as per ERC-191.
296    /// The Ledger asks the user to validate the SHA-256 hash of the message being signed.
297    /// This command has been supported since firmware version 1.0.8
298    ///
299    /// # Arguments:
300    /// message - a string to sign
301    /// hd_path - HD path, prefixed with count of derivation indexes
302    ///
303    /// # Protocol Details:
304    /// According to the Ledger Ethereum specification (reference/ledger-ethereum.adoc),
305    /// message data is sent as:
306    /// - First block: HD path + Message length (4 bytes) + Message chunk
307    /// - Other blocks: Message chunk only
308    ///   The message data requires a 4-byte length prefix (big-endian) before the message content.
309    ///
310    /// # See 
311    /// - https://github.com/LedgerHQ/app-ethereum/blob/d408c161dc43ce4640165464bd8a4f45d662a6f1/doc/ethapp.adoc#sign-eth-transaction
312    /// - https://eips.ethereum.org/EIPS/eip-191
313    pub fn sign_message_erc191(
314        &self,
315        message: String,
316        hd_path: &dyn HDPath,
317    ) -> Result<SignatureBytes, HWKeyError> {
318
319        let message = message.as_bytes();
320        // Prepare message data with 4-byte length prefix as per SIGN ETH PERSONAL MESSAGE specification
321        let mut message_data = Vec::<u8>::with_capacity(message.len() + 4);
322        message_data.extend_from_slice((message.len() as u32).to_be_bytes().as_slice());
323        message_data.extend_from_slice(message);
324        let message_data = message_data.as_slice();
325
326        let res = self.send_chunked_data(COMMAND_SIGN_MESSAGE, hd_path, message_data)?;
327        let signature = SignatureBytes::from_ledger_bytes(res.as_slice())?;
328        debug!("Received signature: {:?}", signature);
329        Ok(signature)
330    }
331
332    /// Sign a message as per EIP-712 (v0 implementation - simple signing with hashes only).
333    /// This implementation requires the domain hash and message hash to be pre-computed
334    /// and provided to the device for signing.
335    /// 
336    /// The Ledger displays the hashes to the user for validation before signing.
337    /// This command has been supported since app version 1.5.0
338    ///
339    /// # Arguments
340    /// * `domain_hash` - The 32-byte domain hash (keccak256 of the domain separator)
341    /// * `message_hash` - The 32-byte message hash (keccak256 of the message)
342    /// * `hd_path` - HD path for the signing key
343    ///
344    /// # Protocol Details
345    /// According to the Ledger Ethereum specification (reference/ledger-ethereum.adoc),
346    /// for the v0 implementation, the data sent is:
347    /// - HD path
348    /// - Domain hash (32 bytes)
349    /// - Message hash (32 bytes)
350    /// 
351    /// # See
352    /// - https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md
353    /// - reference/ledger-ethereum.adoc section "SIGN ETH EIP 712"
354    pub fn sign_message_eip712(
355        &self,
356        domain_hash: &[u8; 32],
357        message_hash: &[u8; 32],
358        hd_path: &dyn HDPath,
359    ) -> Result<SignatureBytes, HWKeyError> {
360        // Prepare the data payload: domain_hash + message_hash
361        let mut data = Vec::<u8>::with_capacity(64);
362        data.extend_from_slice(domain_hash);
363        data.extend_from_slice(message_hash);
364
365        let apdu = ApduBuilder::new(COMMAND_SIGN_EIP712)
366            .with_p1(0x00)
367            // 00 for v0 implementation (simple signing with hashes only)
368            .with_p2(0x00)
369            .with_data(hd_path.to_bytes().as_slice())
370            .with_data(&data)
371            .build();
372
373        let handle = self.ledger.lock().unwrap();
374
375        let res = sendrecv(&*handle, &apdu)?;
376        let signature = SignatureBytes::from_ledger_bytes(res.as_slice())?;
377        debug!("Received EIP-712 signature: {:?}", signature);
378        Ok(signature)
379    }
380
381    pub fn get_version(&self) -> Result<AppVersion, HWKeyError> {
382        let apdu = ApduBuilder::new(COMMAND_APP_CONFIG)
383            .build();
384        let handle = self.ledger.lock().unwrap();
385        let resp = sendrecv(&*handle, &apdu)?;
386        AppVersion::try_from(resp).map_err(|_| HWKeyError::EncodingError("Invalid version config".to_string()))
387    }
388
389    fn is_path_available(&self, hd_path: &dyn HDPath) -> bool {
390        self.get_address(hd_path, false).is_ok_and(|_| true)
391    }
392}
393
394impl PubkeyAddressApp for EthereumApp {
395    fn get_extkey_at(&self, hd_path: &dyn HDPath) -> Result<Box<dyn AsExtendedKey>, HWKeyError> {
396        let address = self.get_address(hd_path, false)?;
397        Ok(Box::new(address))
398    }
399}
400
401#[derive(Copy, Debug, Clone, Eq, PartialEq)]
402pub enum EthereumApps {
403    Ethereum,
404    EthereumClassic
405}
406
407impl LedgerApp for EthereumApp {
408    type Networks = EthereumApps;
409
410    fn new(manager: Arc<Mutex<dyn LedgerTransport>>) -> Self{
411        EthereumApp {
412            ledger: manager
413        }
414    }
415
416    fn is_open(&self) -> Option<Self::Networks> {
417        self.get_version().ok().and_then(|_| {
418            // ETC app gives address for both m/44'/60' and m/44'/61'
419            // but ETH app gives only address for m/44'/60'
420
421            let has_60 = self.is_path_available(
422                &AccountHDPath::try_new(Purpose::Pubkey, 60, 0).expect("no-eth-acc")
423            );
424            let has_61 = self.is_path_available(
425                &AccountHDPath::try_new(Purpose::Pubkey, 61, 0).expect("no-etc-acc")
426            );
427
428            if has_60 && has_61 {
429                Some(EthereumApps::EthereumClassic)
430            } else if has_60 && !has_61 {
431                Some(EthereumApps::Ethereum)
432            } else {
433                None
434            }
435        })
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use crate::ledger::app::ethereum::{AddressResponse, EthereumApp, SignatureBytes, ECDSA_SIGNATURE_BYTES};
442    use crate::ledger::app::LedgerApp;
443    use crate::ledger::connect::direct::CHUNK_SIZE;
444    use crate::ledger::connect::mock::MockTransport;
445    use std::convert::TryFrom;
446    use std::sync::{Arc, Mutex};
447    use hdpath::{StandardHDPath, HDPath};
448
449    #[test]
450    fn chunk_data_small_data() {
451        let transport = Arc::new(Mutex::new(MockTransport::new()));
452        let app = EthereumApp::new(transport);
453        let hd_path = StandardHDPath::try_from("m/44'/60'/0'/0/0").unwrap();
454        
455        let small_data = vec![1, 2, 3, 4, 5];
456        let (init, cont) = app.chunk_data(&small_data, &hd_path);
457        
458        assert_eq!(init, &small_data);
459        assert_eq!(cont.len(), 0);
460    }
461
462    #[test]
463    fn chunk_data_large_data() {
464        let transport = Arc::new(Mutex::new(MockTransport::new()));
465        let app = EthereumApp::new(transport);
466        let hd_path = StandardHDPath::try_from("m/44'/60'/0'/0/0").unwrap();
467        
468        // Create data larger than CHUNK_SIZE
469        let large_data = vec![0u8; CHUNK_SIZE + 100];
470        let (init, cont) = app.chunk_data(&large_data, &hd_path);
471        
472        let expected_init_size = CHUNK_SIZE - hd_path.to_bytes().len();
473        assert_eq!(init.len(), expected_init_size);
474        assert_eq!(cont.len(), large_data.len() - expected_init_size);
475        
476        // Verify the data is correctly split
477        let mut reconstructed = Vec::new();
478        reconstructed.extend_from_slice(init);
479        reconstructed.extend_from_slice(cont);
480        assert_eq!(reconstructed, large_data);
481    }
482
483    #[test]
484    fn chunk_data_exact_chunk_size() {
485        let transport = Arc::new(Mutex::new(MockTransport::new()));
486        let app = EthereumApp::new(transport);
487        let hd_path = StandardHDPath::try_from("m/44'/60'/0'/0/0").unwrap();
488        
489        // Create data exactly CHUNK_SIZE
490        let exact_data = vec![0u8; CHUNK_SIZE];
491        let (init, cont) = app.chunk_data(&exact_data, &hd_path);
492        
493        assert_eq!(init, &exact_data);
494        assert_eq!(cont.len(), 0);
495    }
496
497    #[test]
498    fn send_chunked_data_builds_correct_apdu() {
499        let transport = Arc::new(Mutex::new(MockTransport::new()));
500        let app = EthereumApp::new(transport);
501        let hd_path = StandardHDPath::try_from("m/44'/60'/0'/0/0").unwrap();
502        
503        let small_data = vec![1, 2, 3, 4, 5];
504        let (init, cont) = app.chunk_data(&small_data, &hd_path);
505        
506        // Verify that chunking works as expected for small data
507        assert_eq!(init, &small_data);
508        assert_eq!(cont.len(), 0);
509        
510        // Test with large data
511        let large_data = vec![0u8; CHUNK_SIZE + 100];
512        let (init, cont) = app.chunk_data(&large_data, &hd_path);
513        
514        // Verify chunking works for large data
515        let expected_init_size = CHUNK_SIZE - hd_path.to_bytes().len();
516        assert_eq!(init.len(), expected_init_size);
517        assert_eq!(cont.len(), large_data.len() - expected_init_size);
518        
519        // Verify data integrity
520        let mut reconstructed = Vec::new();
521        reconstructed.extend_from_slice(init);
522        reconstructed.extend_from_slice(cont);
523        assert_eq!(reconstructed, large_data);
524    }
525
526    #[test]
527    fn chunk_data_respects_hd_path_length() {
528        let transport = Arc::new(Mutex::new(MockTransport::new()));
529        let app = EthereumApp::new(transport);
530        
531        // Test with different HD path lengths using CustomHDPath and StandardHDPath
532        use hdpath::CustomHDPath;
533        
534        // CustomHDPath with fewer derivation levels than StandardHDPath
535        let short_path = CustomHDPath::try_from("m/44'/60'/0'/0").unwrap();      // 4 levels
536        let long_path = StandardHDPath::try_from("m/44'/60'/0'/0/0").unwrap();   // 5 levels
537        
538        let test_data = vec![0u8; CHUNK_SIZE + 50];
539        
540        let (init_short, cont_short) = app.chunk_data(&test_data, &short_path);
541        let (init_long, cont_long) = app.chunk_data(&test_data, &long_path);
542        
543        // Verify the expected chunk sizes
544        let short_path_len = short_path.to_bytes().len();
545        let long_path_len = long_path.to_bytes().len();
546        
547        // Short path should have fewer bytes than long path
548        assert!(short_path_len < long_path_len, "Short path should have fewer bytes than long path");
549        
550        let expected_init_short = CHUNK_SIZE - short_path_len;
551        let expected_init_long = CHUNK_SIZE - long_path_len;
552        
553        assert_eq!(init_short.len(), expected_init_short);
554        assert_eq!(init_long.len(), expected_init_long);
555        
556        // With shorter HD path, more data should fit in initial chunk
557        assert!(init_short.len() > init_long.len(), "Shorter path should allow more data in initial chunk");
558        assert!(cont_short.len() < cont_long.len(), "Shorter path should have less continuation data");
559        
560        // Both should reconstruct to the same original data
561        let mut reconstructed_short = Vec::new();
562        reconstructed_short.extend_from_slice(init_short);
563        reconstructed_short.extend_from_slice(cont_short);
564        
565        let mut reconstructed_long = Vec::new();
566        reconstructed_long.extend_from_slice(init_long);
567        reconstructed_long.extend_from_slice(cont_long);
568        
569        assert_eq!(reconstructed_short, test_data);
570        assert_eq!(reconstructed_long, test_data);
571    }
572
573    #[test]
574    fn decode_std_address() {
575        let resp = hex::decode("4104b28217096d8ad3dd25461404c3941a5196ac8f089f1be5bcb62df2ce08a71ba1ca4b879ee38217cced7ef1c9dc5c15cb804ab159503514f73559d1a1192ba1fc28354164343233663565623437333534313563393366306365353266366532633133424436413530309000000000000035140000000000000000000000000000000000000000000000000000000000000000").unwrap();
576        let parsed = AddressResponse::try_from(resp);
577        assert!(parsed.is_ok(), "{:?}", parsed);
578        let parsed = parsed.unwrap();
579        assert_eq!("0x5Ad423f5eb4735415c93f0ce52f6e2c13BD6A500".to_string(), parsed.address);
580        assert_eq!(
581            "04b28217096d8ad3dd25461404c3941a5196ac8f089f1be5bcb62df2ce08a71ba1ca4b879ee38217cced7ef1c9dc5c15cb804ab159503514f73559d1a1192ba1fc",
582            hex::encode(parsed.pubkey.serialize_uncompressed()));
583    }
584
585    #[test]
586    fn decode_std_address_2() {
587        let resp = hex::decode("4104452ae4b222d10cb80c269d0677f7165c548e49113d91b26848ae01a7732f15ff88379573411237d1a9dfb9603d2f40d7a56bf12b1bf5f6ae3b69d7bfebd45689283364363634383362344361643335313838363130323946663836613338376542633437303531373290000000000000f5f60000000000000000000000000000000000000000000000000000000000000000").unwrap();
588        let parsed = AddressResponse::try_from(resp);
589        assert!(parsed.is_ok(), "{:?}", parsed);
590        let parsed = parsed.unwrap();
591        assert_eq!("0x3d66483b4Cad3518861029Ff86a387eBc4705172".to_string(), parsed.address);
592        assert_eq!(
593            "04452ae4b222d10cb80c269d0677f7165c548e49113d91b26848ae01a7732f15ff88379573411237d1a9dfb9603d2f40d7a56bf12b1bf5f6ae3b69d7bfebd45689",
594            hex::encode(parsed.pubkey.serialize_uncompressed()));
595    }
596
597    #[test]
598    fn test_from_ledger_bytes_to_vrs_roundtrip() {
599        let original_data: [u8; ECDSA_SIGNATURE_BYTES] = [
600            0x1f, // V
601            0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11,
602            0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21,
603            0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31,
604            0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41,
605        ];
606
607        let signature = SignatureBytes::from_ledger_bytes(&original_data).expect("Failed to create SignatureBytes");
608
609        assert_eq!(original_data, signature.to_ledger_bytes());
610    }
611}