Skip to main content

emerald_hwkey/ledger/app/
bitcoin.rs

1use std::convert::TryFrom;
2use std::sync::{Arc, Mutex};
3use bitcoin::{
4    Transaction,
5    Address,
6    VarInt,
7    PublicKey,
8    EcdsaSighashType,
9    TxIn,
10    consensus::{serialize},
11    blockdata::{
12        script::Builder,
13        witness::Witness,
14        opcodes
15    },
16    bip32::{ChainCode},
17    ScriptBuf,
18    CompressedPublicKey,
19    WPubkeyHash,
20    NetworkKind,
21    KnownHrp,
22    ecdsa::Signature
23};
24use byteorder::{WriteBytesExt, LittleEndian};
25use hdpath::{StandardHDPath, HDPath};
26use sha2::{Sha256, Digest};
27use ripemd::Ripemd160;
28use crate::{
29    ledger::{
30        comm::LedgerTransport,
31        commons::as_compact,
32        app::{AsPubkey, AsChainCode, PubkeyAddressApp, AsExtendedKey, LedgerApp}
33    }
34};
35use crate::errors::HWKeyError;
36use crate::ledger::apdu::ApduBuilder;
37use crate::ledger::comm::sendrecv;
38use crate::ledger::connect::direct::CHUNK_SIZE;
39
40const COMMAND_GET_ADDRESS: u8 = 0x40;
41const COMMAND_COIN_VERSION: u8 = 0x16;
42#[allow(dead_code)]
43const COMMAND_GET_UNTRUSTED_INPUT: u8 = 0x42;
44const COMMAND_UNTRUSTED_HASH_TX: u8 = 0x44;
45const COMMAND_UNTRUSTED_HASH_SIGN: u8 = 0x48;
46const COMMAND_HASH_INPUT_FINALIZE_FULL: u8 = 0x4A;
47
48#[derive(Copy, Clone, Eq, PartialEq, Debug)]
49#[repr(u8)]
50pub enum AddressType {
51    ///  legacy address
52    Legacy = 0,
53    /// P2SH-P2WPKH address
54    SegwitCompat = 1,
55    // Bech32 encoded P2WPKH address
56    Bench32 = 2
57}
58
59pub struct BitcoinApp {
60    ledger: Arc<Mutex<dyn LedgerTransport>>
61}
62
63#[derive(Debug, Clone, Eq, PartialEq)]
64pub struct AddressResponse {
65    pub pubkey: PublicKey,
66    pub address: Address,
67    pub chaincode: ChainCode
68}
69
70impl AsPubkey for AddressResponse {
71    fn as_pubkey(&self) -> &bitcoin::secp256k1::PublicKey {
72        &self.pubkey.inner
73    }
74}
75
76impl AsChainCode for AddressResponse {
77    fn as_chaincode(&self) -> &ChainCode {
78        &self.chaincode
79    }
80}
81
82impl AsExtendedKey for AddressResponse {}
83
84#[derive(Debug, Clone, Copy, Eq, PartialEq)]
85pub struct GetAddressOpts {
86    pub address_type: AddressType,
87    pub confirmation: bool,
88    pub network: NetworkKind,
89}
90
91impl Default for GetAddressOpts {
92    fn default() -> Self {
93        GetAddressOpts {
94            address_type: AddressType::Bench32,
95            confirmation: false,
96            network: NetworkKind::Main
97        }
98    }
99}
100
101impl GetAddressOpts {
102    pub fn confirm() -> GetAddressOpts {
103        GetAddressOpts {
104            confirmation: true,
105            ..GetAddressOpts::default()
106        }
107    }
108
109    pub fn compat_address() -> GetAddressOpts {
110        GetAddressOpts {
111            address_type: AddressType::SegwitCompat,
112            ..GetAddressOpts::default()
113        }
114    }
115}
116
117pub fn hash160(value: &[u8]) -> [u8; 20] {
118    let mut hash256 = Sha256::new();
119    hash256.update(value);
120    let mut hash160 = Ripemd160::new();
121    hash160.update(hash256.finalize());
122    let bytes = hash160.finalize().to_vec();
123    let mut result = [0u8; 20];
124    result.copy_from_slice(bytes.as_slice());
125    result
126}
127
128impl TryFrom<(Vec<u8>, GetAddressOpts)> for AddressResponse {
129    type Error = HWKeyError;
130
131    fn try_from(full: (Vec<u8>, GetAddressOpts)) -> Result<Self, Self::Error> {
132        let value = full.0;
133        let opts = full.1;
134        if value.is_empty() {
135            return Err(HWKeyError::EncodingError("Empty data".to_string()))
136        }
137        let pubkey_len = value[0] as usize;
138        if 1 + pubkey_len > value.len() {
139            return Err(HWKeyError::EncodingError(
140                format!("Pubkey cutoff. {:?} > {:?} for {:?}", 1 + pubkey_len, value.len(), pubkey_len)
141            ))
142        }
143        let pubkey = &value[1..pubkey_len+1];
144        let pubkey = PublicKey::from_slice(pubkey)
145            .map_err(|_| HWKeyError::CryptoError("Invalid public key".to_string()))?;
146        let pubkey_comp = PublicKey::new(as_compact(&pubkey.inner)?);
147
148        let address_len = value[pubkey_len + 1] as usize;
149        let address_start = 1 + pubkey_len + 1;
150        let address_end = address_start + address_len;
151        if address_end > value.len() {
152            return Err(HWKeyError::EncodingError(
153                format!("Address cutoff. {:?} > {:?} as {:?}..{:?} for {:?}", address_end, value.len(), address_start, address_end, address_len)
154            ))
155        }
156
157        let address = match opts.address_type {
158            AddressType::Bench32 => {
159                let compressed = CompressedPublicKey::try_from(pubkey_comp)
160                    .map_err(|_| HWKeyError::CryptoError("Invalid public key".to_string()))?;
161                let hrp = match opts.network {
162                    NetworkKind::Main => KnownHrp::Mainnet,
163                    NetworkKind::Test => KnownHrp::Testnets
164                };
165                Address::p2wpkh(&compressed, hrp)
166            },
167            AddressType::SegwitCompat => {
168                let script = Builder::new()
169                    .push_opcode(opcodes::all::OP_PUSHBYTES_0)
170                    .push_slice(hash160(pubkey_comp.to_bytes().as_slice()))
171                    .into_script();
172                Address::p2sh(&script, opts.network)
173                    .map_err(|_| HWKeyError::CryptoError("Invalid Pubkey".to_string()))?
174            },
175            AddressType::Legacy => Address::p2pkh(pubkey_comp, opts.network)
176        };
177
178        let chaincode_len = 32usize;
179        let chaincode_start = address_end;
180        let chaincode_end = chaincode_start + chaincode_len;
181        if chaincode_end > value.len() {
182            return Err(HWKeyError::EncodingError(
183                format!("Chaincode cutoff. {:?} > {:?}", chaincode_end, value.len())
184            ))
185        }
186        let chaincode = value[chaincode_start..chaincode_end].to_vec();
187        let chaincode = ChainCode::try_from(chaincode.as_slice()).unwrap();
188
189        Ok(AddressResponse {
190            pubkey: pubkey_comp, address, chaincode
191        })
192    }
193}
194
195#[derive(Clone)]
196pub struct SignTx {
197    pub network: NetworkKind,
198    pub inputs: Vec<UnsignedInput>,
199}
200
201#[derive(Clone)]
202pub struct UnsignedInput {
203    pub index: usize,
204    pub amount: u64,
205    pub hd_path: StandardHDPath
206}
207
208#[derive(Clone)]
209struct InputDetails {
210    prev_tx: TxIn,
211    amount: u64,
212    from_address: AddressResponse,
213    redeem: ScriptBuf,
214    hd_path: StandardHDPath
215}
216
217impl BitcoinApp {
218
219    /// Get address
220    ///
221    /// # Arguments:
222    /// hd_path - HD path, prefixed with count of derivation indexes
223    ///
224    pub fn get_address(&self, hd_path: &dyn HDPath, opts: GetAddressOpts) -> Result<AddressResponse, HWKeyError> {
225        let mut handle = self.ledger.lock().unwrap();
226        BitcoinApp::get_address_internal(&mut *handle, hd_path, opts)
227    }
228
229    fn get_address_internal(device: &mut dyn LedgerTransport, hd_path: &dyn HDPath, opts: GetAddressOpts) -> Result<AddressResponse, HWKeyError> {
230        log::trace!("Get address for {:?}", hd_path.as_custom());
231        let apdu = ApduBuilder::new(COMMAND_GET_ADDRESS)
232            .with_data(hd_path.to_bytes().as_slice())
233            .with_p1(if opts.confirmation {1} else {0})
234            .with_p2(opts.address_type as u8)
235            .build();
236        sendrecv(device, &apdu)
237            .and_then(|res| AddressResponse::try_from((res, opts)))
238    }
239
240    fn witness_redeem(pubkey: &PublicKey) -> ScriptBuf {
241        let compressed = CompressedPublicKey::try_from(*pubkey)
242            .map_err(|_| HWKeyError::CryptoError("Invalid public key".to_string())).unwrap();
243        let key_hash = WPubkeyHash::from(&compressed);
244        ScriptBuf::p2wpkh_script_code(key_hash)
245    }
246
247    /// Supports only Trusted Segwit tx.
248    /// Trusted Segwit is supported only after 1.4 of the Ledger Firmware
249    pub fn sign_tx(&self, tx: &mut Transaction, config: &SignTx) -> Result<Vec<Signature>, HWKeyError> {
250        let mut device = self.ledger.lock().unwrap();
251        // Protocol:
252        // 1. The transaction shall be processed first with all inputs having a null script length
253        //    (to be done twice if the dongle has been powercycled to retrieve the authorization code)
254        // 2. Then each input to sign shall be processed as part of a pseudo transaction with a single
255        //    input and no outputs.
256
257        let mut inputs: Vec<InputDetails> = Vec::with_capacity(tx.input.len());
258
259        for ui in config.inputs.iter() {
260            let address = BitcoinApp::get_address_internal(&mut *device, &ui.hd_path, GetAddressOpts {
261                network: config.network,
262                ..GetAddressOpts::default()
263            })?;
264
265            inputs.push(InputDetails {
266                prev_tx: tx.input[ui.index].clone(),
267                amount: ui.amount,
268                from_address: address.clone(),
269                hd_path: ui.hd_path.clone(),
270                redeem: BitcoinApp::witness_redeem(&address.pubkey)
271            });
272        }
273
274        self.start_untrusted_hash_tx(&mut *device, true, &inputs, tx, false)?;
275
276        // finalize to get hash
277        self.finalize_outputs(&mut *device, tx)?;
278
279        // make actual signatures
280        let mut signatures = Vec::with_capacity(inputs.len());
281        for (i, input) in inputs.iter().enumerate() {
282            let ic = input.clone();
283            self.start_untrusted_hash_tx(&mut *device, false,&vec![ic], tx, true)?;
284            let signature = self.untrusted_hash_sign(&mut *device, input, tx.lock_time.to_consensus_u32())?;
285            tx.input[i].witness = Witness::p2wpkh(&signature, &input.from_address.pubkey.inner);
286            signatures.push(signature);
287        }
288
289        Ok(signatures)
290    }
291
292    // see reference/ledger-bitcoin.adoc#untrusted-hash-transaction-input-start
293    fn start_untrusted_hash_tx(&self, device: &mut dyn LedgerTransport, is_new_tx: bool, inputs: &[InputDetails], tx: &Transaction, second_pass: bool) -> Result<(), HWKeyError> {
294        let mut data: Vec<u8> = Vec::new();
295        // needs the version
296        data.write_u32::<LittleEndian>(tx.version.0 as u32)
297            .map_err(|_| HWKeyError::EncodingError("Failed to encode version".to_string()))?;
298        data.extend_from_slice(serialize(&VarInt(inputs.len() as u64)).as_slice());
299        for ti in inputs.iter() {
300            // 0x02 if the input is passed as a Segregated Witness Input
301            data.push(0x02);
302            // original 36 bytes prevout
303            data.extend_from_slice(serialize(&ti.prev_tx.previous_output).as_slice());
304            // and the original 8-bytes little endian amount associated with this input
305            data.write_u64::<LittleEndian>(ti.amount)
306                .map_err(|_| HWKeyError::EncodingError("Failed to encode amount".to_string()))?;
307
308            // The transaction shall be processed first with all inputs having a null script length.
309            // Then each input to sign shall be processed as part of a pseudo transaction with a single input and no outputs.
310            // I.e. include scripts only on second pass
311            if second_pass {
312                // must be witness redeem
313                // serialize() encodes size
314                data.extend_from_slice(serialize(&ti.redeem).as_slice());
315            } else {
316                // provide only 0-size
317                data.extend_from_slice(serialize(&VarInt(0u64)).as_slice());
318            };
319            // sequence
320            data.extend_from_slice(serialize(&ti.prev_tx.sequence).as_slice());
321        }
322
323
324        let outputs_count: u64 = if second_pass {
325            // no outputs on second pass
326            0
327        } else {
328            tx.output.len() as u64
329        };
330        data.extend_from_slice(serialize(&VarInt(outputs_count)).as_slice());
331
332
333        let data = data.chunks(CHUNK_SIZE - 28);
334        for (i, chunk) in data.enumerate() {
335            let first = i == 0;
336            // 00 : first transaction data block
337            // 80 : subsequent transaction data block
338            let p1 = if first {
339                0x00
340            } else {
341                0x80
342            };
343            // for the first block only:
344            // 00 : start signing a new transaction
345            // 02 : start signing a new transaction containing Segregated Witness Inputs
346            // 80 : continue signing another input of the current transaction
347            let p2 = if first {
348                if is_new_tx {
349                    0x02
350                } else {
351                    0x80
352                }
353            } else {
354                0x00
355            };
356            let apdu = ApduBuilder::new(COMMAND_UNTRUSTED_HASH_TX)
357                .with_p1(p1)
358                .with_p2(p2)
359                .with_data(chunk)
360                .build();
361            sendrecv(device, &apdu)?;
362        }
363        Ok(())
364    }
365
366    fn untrusted_hash_sign(&self, device: &mut dyn LedgerTransport, input: &InputDetails, locktime: u32) -> Result<Signature, HWKeyError> {
367        let mut data: Vec<u8> = Vec::new();
368        data.extend_from_slice(input.hd_path.to_bytes().as_slice());
369        data.push(0x00); // RFU (0x00)
370        data.write_u32::<LittleEndian>(locktime) //locktime
371            .map_err(|_| HWKeyError::EncodingError("Failed to encode locktime".to_string()))?;
372        data.push(EcdsaSighashType::All as u8); //SigHashType
373        let apdu = ApduBuilder::new(COMMAND_UNTRUSTED_HASH_SIGN)
374            .with_p1(0x00)
375            .with_p2(0x00)
376            .with_data(data.as_slice())
377            .build();
378        let mut signature = sendrecv(device, &apdu)?;
379        // Mask first byte with 0xFE as per Ledger specification
380        // This ensures the DER sequence tag is valid (0x30 instead of potentially 0x31)
381        if !signature.is_empty() {
382            signature[0] &= 0xFE;
383        }
384        Signature::from_slice(signature.as_slice())
385            .map_err(|_| HWKeyError::CryptoError("Received invalid signature from Ledger".to_string()))
386    }
387
388    fn finalize_outputs(&self, device: &mut dyn LedgerTransport, tx: &Transaction) -> Result<(), HWKeyError> {
389        let mut data: Vec<u8> = Vec::new();
390        data.extend_from_slice(serialize(&VarInt(tx.output.len() as u64)).as_slice());
391        for output in &tx.output {
392            data.write_u64::<LittleEndian>(output.value.to_sat())
393                .map_err(|_| HWKeyError::EncodingError("Failed to encode amount".to_string()))?;
394            let script = &output.script_pubkey;
395            // serialize() encodes script size
396            data.extend_from_slice(serialize(script).as_slice());
397        }
398
399        let data = data.chunks(CHUNK_SIZE - 28);
400        let data_len = data.len();
401        for (i, chunk) in data.enumerate() {
402            let last =  i == data_len - 1;
403            let apdu = ApduBuilder::new(COMMAND_HASH_INPUT_FINALIZE_FULL)
404                // 00 : more input data to be sent
405                // 80 : last input data block to be sent
406                // FF : BIP 32 path specified for the change address
407                .with_p1(if !last {0x00} else {0x80})
408                .with_p2(0x00)
409                .with_data(chunk)
410                .build();
411            let result = sendrecv(device, &apdu)?;
412            if last && result.ne(&vec![0x00u8, 0x00u8]) {
413                return Err(HWKeyError::CryptoError("Validation required".to_string()))
414            }
415        }
416        Ok(())
417    }
418
419    fn get_version(&self) -> Option<AppVersion> {
420        let apdu = ApduBuilder::new(COMMAND_COIN_VERSION)
421            .build();
422        let device = self.ledger.lock().unwrap();
423        let resp = sendrecv(&*device, &apdu);
424        if resp.is_err() {
425            return None
426        }
427        AppVersion::try_from(resp.unwrap()).ok()
428    }
429}
430
431impl PubkeyAddressApp for BitcoinApp {
432    fn get_extkey_at(&self, hd_path: &dyn HDPath) -> Result<Box<dyn AsExtendedKey>, HWKeyError> {
433        let address = self.get_address(hd_path, GetAddressOpts {
434            confirmation: false,
435            ..GetAddressOpts::default()
436        })?;
437        Ok(Box::new(address))
438    }
439}
440
441#[derive(Copy, Clone, Eq, PartialEq, Debug)]
442pub enum BitcoinApps {
443    Mainnet,
444    Testnet
445}
446
447#[derive(Clone, Eq, PartialEq, Debug)]
448pub struct AppVersion {
449    p2pkh: [u8; 2],
450    p2sh: [u8; 2],
451    family: u8,
452    name: String,
453    ticker: String
454}
455
456impl TryFrom<Vec<u8>> for AppVersion {
457    type Error = ();
458
459    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
460        let mut expected_len = 2 + 2 + 1 + 1;
461        if value.len() < expected_len {
462            return Err(())
463        }
464        let p2pkh: [u8; 2] = [value[0], value[1]];
465        let p2sh: [u8; 2] = [value[2], value[3]];
466        let family = value[4];
467        let name_len = value[5] as usize;
468        expected_len += name_len;
469        if value.len() < expected_len {
470            return Err(())
471        }
472        let name = String::from_utf8(value[6..6+name_len].to_vec()).map_err(|_| ())?;
473        let ticker_len = value[6 + name_len] as usize;
474        let ticker_start = 6 + name_len + 1;
475        expected_len += ticker_len;
476        if value.len() < expected_len {
477            return Err(())
478        }
479        let ticker = String::from_utf8(value[ticker_start..ticker_start+ticker_len].to_vec()).map_err(|_| ())?;
480        Ok(AppVersion {
481            p2pkh, p2sh,
482            family,
483            name, ticker
484        })
485    }
486}
487
488impl LedgerApp for BitcoinApp {
489    type Networks = BitcoinApps;
490
491    fn new(manager: Arc<Mutex<dyn LedgerTransport>>) -> Self {
492        BitcoinApp { ledger: manager }
493    }
494
495    fn is_open(&self) -> Option<Self::Networks> {
496        self.get_version().and_then(|ver| {
497            if ver.family == 1 && ver.name == "Bitcoin" {
498                match ver.ticker.as_str() {
499                    "BTC" => Some(BitcoinApps::Mainnet),
500                    "TEST" => Some(BitcoinApps::Testnet),
501                    _ => None
502                }
503            } else {
504                None
505            }
506        })
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use crate::ledger::app::bitcoin::{AddressResponse, GetAddressOpts, AppVersion};
513    use std::convert::TryFrom;
514    use bitcoin::Address;
515    use std::str::FromStr;
516
517    #[test]
518    fn decode_btc_app() {
519        let resp = hex::decode("000000050107426974636f696e03425443").unwrap();
520        let app_ver = AppVersion::try_from(resp);
521        assert!(app_ver.is_ok());
522        assert_eq!(
523            AppVersion {
524                p2pkh: [0, 0],
525                p2sh: [0, 5],
526                family: 1,
527                name: "Bitcoin".to_string(),
528                ticker: "BTC".to_string()
529            },
530            app_ver.unwrap()
531        )
532    }
533
534    #[test]
535    fn decode_btctest_app() {
536        let resp = hex::decode("000000050107426974636f696e0454455354").unwrap();
537        let app_ver = AppVersion::try_from(resp);
538        assert!(app_ver.is_ok());
539        assert_eq!(
540            AppVersion {
541                p2pkh: [0, 0],
542                p2sh: [0, 5],
543                family: 1,
544                name: "Bitcoin".to_string(),
545                ticker: "TEST".to_string()
546            },
547            app_ver.unwrap()
548        )
549    }
550
551    #[test]
552    fn decode_segwit_address_1() {
553        let resp = hex::decode("410465fa75cc427606b99d9aaa326fdc7d0d30add37c545c5795eab1112839ccb406198798942cc6ccac5cc1933b584b23a82f66278513f38a4765e0cdf44b11d5eb2a6263317161616179796b7272783834636c676e706366717530306e6d663267336d6637663533706b336ee115bac4f8c9019b63a1dbec0edf5c22ed14bf94508ff082926964c123c0906c9000000000000000000000000000000000000000000000000000000000000000c901").unwrap();
554        let parsed = AddressResponse::try_from((resp, GetAddressOpts::default()));
555        assert!(parsed.is_ok(), "{:?}", parsed);
556        let parsed = parsed.unwrap();
557        assert_eq!(Address::from_str("bc1qaaayykrrx84clgnpcfqu00nmf2g3mf7f53pk3n").unwrap().assume_checked(), parsed.address);
558        assert_eq!(
559            "0365fa75cc427606b99d9aaa326fdc7d0d30add37c545c5795eab1112839ccb406",
560            hex::encode(parsed.pubkey.to_bytes()));
561        assert_eq!(
562            "e115bac4f8c9019b63a1dbec0edf5c22ed14bf94508ff082926964c123c0906c",
563            hex::encode(parsed.chaincode.as_bytes())
564        )
565    }
566
567    #[test]
568    fn decode_segwit_address_2() {
569        let resp = hex::decode("410423e3b63f8bfec04e968b6b413242006e59e74972617543325116d836521fadb548bac4825b5175c971a4bcae42d75ba622f130048860099a2548980e6e9c06402a6263317175746e616c63776a6561397a6633387667637a6b6e6377387376646339677a79736c6176776e40b2f931e05f7d88850de2ca6f3a5cb68a95740139944d8e5fb91f7b6e23772090000000000000000000000000000000000000000000000000000000000000005f7d").unwrap();
570        let parsed = AddressResponse::try_from((resp, GetAddressOpts::default()));
571        assert!(parsed.is_ok(), "{:?}", parsed);
572        let parsed = parsed.unwrap();
573        assert_eq!(Address::from_str("bc1qutnalcwjea9zf38vgczkncw8svdc9gzyslavwn").unwrap().assume_checked(), parsed.address);
574        assert_eq!(
575            "0223e3b63f8bfec04e968b6b413242006e59e74972617543325116d836521fadb5",
576            hex::encode(parsed.pubkey.to_bytes()));
577        assert_eq!(
578            "40b2f931e05f7d88850de2ca6f3a5cb68a95740139944d8e5fb91f7b6e237720",
579            hex::encode(parsed.chaincode.as_bytes())
580        )
581    }
582
583    #[test]
584    fn decode_segwit_address_3() {
585        let resp = hex::decode("4104cbf9b7ef45036927be859f4d0125f404ef1247878fb97c2b11c05726df0f2323833595ea361631ffeef009b8fa760073a7943a904e04b5dca373fdfd91b1d8342a626331717472346d37776d33336334777a79776833746774706b6b706430776e64326c6d79797166396d8ea6ceaac3341fd23f07c23702ab4303683cce2ddb9d8a4bdb080d4c27b53cae9000000000000000000000000000000000000000000000000000000000000000341f").unwrap();
586        let parsed = AddressResponse::try_from((resp, GetAddressOpts::default()));
587        assert!(parsed.is_ok(), "{:?}", parsed);
588        let parsed = parsed.unwrap();
589        assert_eq!(Address::from_str("bc1qtr4m7wm33c4wzywh3tgtpkkpd0wnd2lmyyqf9m").unwrap().assume_checked(), parsed.address);
590        assert_eq!(
591            "02cbf9b7ef45036927be859f4d0125f404ef1247878fb97c2b11c05726df0f2323",
592            hex::encode(parsed.pubkey.to_bytes()));
593        assert_eq!(
594            "8ea6ceaac3341fd23f07c23702ab4303683cce2ddb9d8a4bdb080d4c27b53cae",
595            hex::encode(parsed.chaincode.as_bytes())
596        )
597    }
598
599    #[test]
600    fn decode_compat_address_1() {
601        let resp = hex::decode("41047311bac2b7908931e73f5b8d02ca9cf8ff294bfad6d2e1e5bba707757d97be3591b954c37b9db706700667d9c15ec31d11053bcc644102fee05f2331c4f28b82223336725948586a72517035754a56665a666457355933467671474446445668746d73dae818a01fbfce0d8bf2deaae7d462a6a79a3be90ec011a79c65ec7251ffab2c90000000000000000000000000000000000000000000000000000000000000000000000000000000d462").unwrap();
602        let parsed = AddressResponse::try_from((resp, GetAddressOpts::compat_address()));
603        assert!(parsed.is_ok(), "{:?}", parsed);
604        let parsed = parsed.unwrap();
605        assert_eq!(Address::from_str("36rYHXjrQp5uJVfZfdW5Y3FvqGDFDVhtms").unwrap().assume_checked(), parsed.address);
606        assert_eq!(
607            "027311bac2b7908931e73f5b8d02ca9cf8ff294bfad6d2e1e5bba707757d97be35",
608            hex::encode(parsed.pubkey.to_bytes()));
609        assert_eq!(
610            "dae818a01fbfce0d8bf2deaae7d462a6a79a3be90ec011a79c65ec7251ffab2c",
611            hex::encode(parsed.chaincode.as_bytes())
612        )
613    }
614}