mod utils; use std::str::FromStr; use bitcoin::{ bip32::DerivationPath, hashes::{hex::FromHex, Hash}, psbt::Psbt, }; use ledger_bitcoin_client::{async_client, client, psbt::PartialSignature, wallet}; fn test_cases(path: &str) -> Vec { let data = std::fs::read_to_string(path).expect("Unable to read file"); serde_json::from_str(&data).expect("Wrong tests data") } #[tokio::test] async fn test_get_version() { let exchanges: Vec = vec![ "=> b001000000".into(), "<= 010c426974636f696e205465737405322e312e3001009000".into(), ]; let store = utils::RecordStore::new(&exchanges); let (name, version, flags) = client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .get_version() .unwrap(); assert_eq!(name, "Bitcoin Test".to_string()); assert_eq!(version, "2.1.0".to_string()); assert_eq!(flags, vec![0x00]); let (name, version, flags) = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .get_version() .await .unwrap(); assert_eq!(name, "Bitcoin Test".to_string()); assert_eq!(version, "2.1.0".to_string()); assert_eq!(flags, vec![0x00]); } #[tokio::test] async fn test_sign_message() { let exchanges: Vec = vec![ "=> e110000132048000002c800000018000000000000000058a2a5c9b768827de5a9552c38a044c66959c68f6d2f21b5260af54d2f87db827".into(), "<= 418a2a5c9b768827de5a9552c38a044c66959c68f6d2f21b5260af54d2f87db8270100e000".into(), "=> f8010001228a2a5c9b768827de5a9552c38a044c66959c68f6d2f21b5260af54d2f87db8270000".into(), "<= 40008a2a5c9b768827de5a9552c38a044c66959c68f6d2f21b5260af54d2f87db827e000".into(), "=> f80100010806060068656c6c6f".into(), "<= 20bdeef462c0ce01b905db5206a51ed05a36671d1494ac12b18c764dbb955f45542c5819611050096d16ed03a5b01fc9806c163619777986235ed75fc91ee933e69000".into(), ]; let path = DerivationPath::from_str("m/44'/1'/0'/0").unwrap(); let store = utils::RecordStore::new(&exchanges); let (header, ecdsa_sig) = client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .sign_message("hello".as_bytes(), &path) .unwrap(); assert_eq!(header, 0x20); let mut sig = vec![header]; sig.extend(ecdsa_sig.serialize_compact()); assert_eq!( "IL3u9GLAzgG5BdtSBqUe0Fo2Zx0UlKwSsYx2TbuVX0VULFgZYRBQCW0W7QOlsB/JgGwWNhl3eYYjXtdfyR7pM+Y=", base64::encode(sig) ); let (header, ecdsa_sig) = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .sign_message("hello".as_bytes(), &path) .await .unwrap(); assert_eq!(header, 0x20); let mut sig = vec![header]; sig.extend(ecdsa_sig.serialize_compact()); assert_eq!( "IL3u9GLAzgG5BdtSBqUe0Fo2Zx0UlKwSsYx2TbuVX0VULFgZYRBQCW0W7QOlsB/JgGwWNhl3eYYjXtdfyR7pM+Y=", base64::encode(sig) ); } #[tokio::test] async fn test_get_extended_pubkey() { for case in test_cases("./tests/data/get_extended_pubkey.json") { let exchanges: Vec = case .get("exchanges") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let derivation_path: DerivationPath = case .get("derivation_path") .map(|v| v.as_str().unwrap()) .map(|s| DerivationPath::from_str(&s).unwrap()) .unwrap(); let display: bool = case .get("display") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let xpk_str: String = case .get("result") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let store = utils::RecordStore::new(&exchanges); let key = client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .get_extended_pubkey(&derivation_path, display) .unwrap(); assert_eq!(key.to_string(), xpk_str); let key = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .get_extended_pubkey(&derivation_path, display) .await .unwrap(); assert_eq!(key.to_string(), xpk_str); } } #[tokio::test] async fn test_register_wallet() { for case in test_cases("./tests/data/register_wallet.json") { let exchanges: Vec = case .get("exchanges") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let name: String = case .get("name") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let policy: String = case .get("policy") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let keys_str: Vec = case .get("keys") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let keys: Vec = keys_str .iter() .map(|s| wallet::WalletPubKey::from_str(s).unwrap()) .collect(); let hmac_result: String = case .get("hmac") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let version: usize = case .get("version") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let version = if version == 1 { wallet::Version::V1 } else { wallet::Version::V2 }; let wallet = wallet::WalletPolicy::new(name, version, policy, keys); let store = utils::RecordStore::new(&exchanges); let (_id, hmac) = client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .register_wallet(&wallet) .unwrap(); assert_eq!(hmac, <[u8; 32]>::from_hex(&hmac_result).unwrap()); let (_id, hmac) = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .register_wallet(&wallet) .await .unwrap(); assert_eq!(hmac, <[u8; 32]>::from_hex(&hmac_result).unwrap()); } } #[tokio::test] async fn test_get_wallet_address() { for case in test_cases("./tests/data/get_wallet_address.json") { let exchanges: Vec = case .get("exchanges") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let name: String = case .get("name") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let policy: String = case .get("policy") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let keys_str: Vec = case .get("keys") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let keys: Vec = keys_str .iter() .map(|s| wallet::WalletPubKey::from_str(s).unwrap()) .collect(); let hmac: Option = case .get("hmac") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let hmac = hmac.map(|s| { let mut h = [b'\0'; 32]; h.copy_from_slice(&Vec::from_hex(&s).unwrap()); h }); let change: bool = case .get("change") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let display: bool = case .get("display") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let address_index: u32 = case .get("address_index") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let address_result: String = case .get("address") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let wallet = wallet::WalletPolicy::new(name, wallet::Version::V2, policy, keys); let store = utils::RecordStore::new(&exchanges); let address = client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .get_wallet_address(&wallet, hmac.as_ref(), change, address_index, display) .unwrap(); assert_eq!(address.assume_checked().to_string(), address_result); let address = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .get_wallet_address(&wallet, hmac.as_ref(), change, address_index, display) .await .unwrap(); assert_eq!(address.assume_checked().to_string(), address_result); } } #[tokio::test] async fn test_sign_psbt() { for case in test_cases("./tests/data/sign_psbt.json") { let exchanges: Vec = case .get("exchanges") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let name: String = case .get("name") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let policy: String = case .get("policy") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let keys_str: Vec = case .get("keys") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let keys: Vec = keys_str .iter() .map(|s| wallet::WalletPubKey::from_str(s).unwrap()) .collect(); let hmac: Option = case .get("hmac") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let hmac = hmac.map(|s| { let mut h = [b'\0'; 32]; h.copy_from_slice(&Vec::from_hex(&s).unwrap()); h }); let sigs: Vec = case .get("sigs") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let psbt_str: String = case .get("psbt") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); let psbt = Psbt::deserialize(&base64::decode(&psbt_str).unwrap()).unwrap(); let wallet = wallet::WalletPolicy::new(name, wallet::Version::V2, policy, keys); let store = utils::RecordStore::new(&exchanges); let res = client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .sign_psbt(&psbt, &wallet, hmac.as_ref()) .unwrap(); let check_signatures = |sigs: &[serde_json::Value], res: Vec<(usize, PartialSignature)>| { for (i, psbt_sig) in res { for (j, res_sig) in sigs.iter().enumerate() { if i == j { match psbt_sig { PartialSignature::TapScriptSig(key, tapleaf_hash, sig) => { assert_eq!( res_sig .get("key") .map(|v| serde_json::from_value::(v.clone()) .unwrap()) .unwrap(), key.to_string() ); if let Some(tapleaf_hash_res) = res_sig .get("tapleaf_hash") .map(|v| serde_json::from_value::(v.clone()).unwrap()) { assert_eq!( tapleaf_hash_res, hex::encode(tapleaf_hash.unwrap().to_byte_array()) ); } assert_eq!( res_sig .get("sig") .map(|v| serde_json::from_value::(v.clone()) .unwrap()) .unwrap(), hex::encode(sig.to_vec()) ); } _ => {} } } } } }; check_signatures(&sigs, res); let res = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .sign_psbt(&psbt, &wallet, hmac.as_ref()) .await .unwrap(); check_signatures(&sigs, res); } }