diff --git a/.github/workflows/build-release-android.yml b/.github/workflows/build-release-android.yml index eb7cfe86..43e0a564 100644 --- a/.github/workflows/build-release-android.yml +++ b/.github/workflows/build-release-android.yml @@ -15,7 +15,7 @@ jobs: build: name: Build Android Release if: github.event.review.state == 'approved' - runs-on: macos-12 + runs-on: macos-13 steps: - name: Get the latest commit SHA id: sha diff --git a/.github/workflows/build-release-ios.yml b/.github/workflows/build-release-ios.yml index 4793c399..3ea24d03 100644 --- a/.github/workflows/build-release-ios.yml +++ b/.github/workflows/build-release-ios.yml @@ -14,7 +14,7 @@ jobs: build: name: Build iOS Release if: github.event.review.state == 'approved' - runs-on: macos-12 + runs-on: macos-13 steps: - name: Get the latest commit SHA id: sha diff --git a/.github/workflows/run-unittest.yml b/.github/workflows/run-unittest.yml new file mode 100644 index 00000000..2bbe1edb --- /dev/null +++ b/.github/workflows/run-unittest.yml @@ -0,0 +1,64 @@ +name: Run Tcx Unit Testing + +on: + pull_request: + types: + - opened + - synchronize + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.sha }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + + +jobs: + build: + name: Run Tcx Unit Testing + runs-on: macos-13 + steps: + - name: Get the latest commit SHA + id: sha + uses: actions/github-script@v6 + with: + result-encoding: string + script: | + const { owner, repo, number } = context.issue + const pr = await github.rest.pulls.get({ + owner, + repo, + pull_number: number, + }) + return pr.data.head.sha + + - name: Checkout repository + uses: actions/checkout@v3 + with: + ref: ${{ steps.sha.outputs.result }} + fetch-depth: 5 + + - name: Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + ~/.rustup + target + key: ${{ runner.os }}-nightly + + - name: Install Rust + run: | + rustup toolchain install nightly-2022-10-31 + rustup default nightly-2022-10-31-x86_64-apple-darwin + rustup show + + - name: Install dependency + run: | + brew install protobuf + + - name: Run TCX Unit Testing + run: | + make test-tcx diff --git a/Cargo.lock b/Cargo.lock index 1a355dd3..ad3526d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -564,6 +564,7 @@ name = "coin-bitcoin" version = "0.1.0" dependencies = [ "anyhow", + "bech32 0.9.1", "bitcoin", "bitcoin_hashes", "bytes", @@ -2001,7 +2002,6 @@ dependencies = [ "prost", "prost-types", "regex", - "rustc-serialize", "secp256k1", "serde", "serde_derive", @@ -2724,9 +2724,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.62" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -2756,9 +2756,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.98" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -3384,12 +3384,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" -[[package]] -name = "rustc-serialize" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" - [[package]] name = "rustc_version" version = "0.4.0" @@ -4350,6 +4344,7 @@ dependencies = [ "base64 0.13.1", "bech32 0.9.1", "bytes", + "lazy_static", "prost", "tcx-common", "tcx-constants", diff --git a/VERSION b/VERSION index a4dd9dba..834f2629 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.4 +2.8.0 diff --git a/imkey-core/ikc-common/Cargo.toml b/imkey-core/ikc-common/Cargo.toml index a3dc774a..58a4b825 100644 --- a/imkey-core/ikc-common/Cargo.toml +++ b/imkey-core/ikc-common/Cargo.toml @@ -10,7 +10,6 @@ edition = "2018" hyper = { version = "=0.14.23", features = ["full"] } hyper-tls = "=0.5.0" hyper-timeout = "=0.4.1" -rustc-serialize = "=0.3.24" serde = { version = "=1.0.147", features = ["derive"] } serde_derive = "=1.0.147" serde_json = "=1.0.89" diff --git a/imkey-core/ikc-common/src/apdu.rs b/imkey-core/ikc-common/src/apdu.rs index a2b01cd2..b0daf2ae 100644 --- a/imkey-core/ikc-common/src/apdu.rs +++ b/imkey-core/ikc-common/src/apdu.rs @@ -1,8 +1,8 @@ use crate::constants::{BTC_AID, COSMOS_AID, EOS_AID, ETH_AID, LC_MAX}; use crate::error::ApduError; +use crate::hex::ToHex; use crate::Result; use hex; -use rustc_serialize::hex::ToHex; pub trait CoinCommonApdu: Default { fn select_applet() -> String; @@ -69,6 +69,19 @@ impl BtcApdu { apdu.to_hex().to_uppercase() } + /** + *p2 00:sign psbt transaction 80: sign message + **/ + pub fn btc_psbt_preview(data: &Vec, p2: u8) -> String { + if data.len() as u32 > LC_MAX { + panic!("data to long"); + } + let mut apdu = ApduHeader::new(0x80, 0x4C, 0x00, p2, data.len() as u8).to_array(); + apdu.extend(data.iter()); + apdu.push(0x00); + apdu.to_hex().to_uppercase() + } + pub fn btc_sign(index: u8, hash_type: u8, path: &str) -> String { let path_bytes = path.as_bytes(); let mut apdu = @@ -108,6 +121,21 @@ impl BtcApdu { apdu.to_hex().to_uppercase() } + pub fn btc_taproot_script_sign(last_one: bool, data: Vec) -> String { + if data.len() as u32 > LC_MAX { + panic!("data to long"); + } + + let mut apdu = match last_one { + true => ApduHeader::new(0x80, 0x40, 0x80, 0x80, data.len() as u8).to_array(), + _ => ApduHeader::new(0x80, 0x40, 0x00, 0x80, data.len() as u8).to_array(), + }; + + apdu.extend(data.iter()); + apdu.push(0x00); + apdu.to_hex().to_uppercase() + } + pub fn omni_prepare_data(p1: u8, data: Vec) -> String { if data.len() as u32 > LC_MAX { panic!("data to long"); @@ -629,6 +657,7 @@ impl ApduCheck { "F080" => Err(ApduError::ImkeyInMenuPage.into()), "F081" => Err(ApduError::ImkeyPinNotVerified.into()), "6F01" => Err(ApduError::ImkeyBluetoothChannelError.into()), + "6943" => Err(ApduError::ImkeyMnemonicCheckFail.into()), _ => Err(anyhow!("imkey_command_execute_fail_{}", response_data)), //Err(ApduError::ImkeyCommandExecuteFail.into()) } } diff --git a/imkey-core/ikc-common/src/applet.rs b/imkey-core/ikc-common/src/applet.rs index ac741b35..435f18e9 100644 --- a/imkey-core/ikc-common/src/applet.rs +++ b/imkey-core/ikc-common/src/applet.rs @@ -1,6 +1,6 @@ use crate::constants::{ - BCH_AID, BTC_AID, COSMOS_AID, EOS_AID, ETH_AID, FILECOIN_AID, IMK_AID, KUSAMA_AID, LTC_AID, - NERVOS_AID, POLKADOT_AID, TEZOS_AID, TRON_AID, + BCH_AID, BTC_AID, COSMOS_AID, DOGECOIN_AID, EOS_AID, ETH_AID, FILECOIN_AID, IMK_AID, + KUSAMA_AID, LTC_AID, NERVOS_AID, POLKADOT_AID, TEZOS_AID, TRON_AID, }; // type __appletName = 'IMK' | 'Ethereum' | 'Bitcoin' | 'EOS' | 'Cosmos' | 'Filecoin' | 'Kusama' | 'Tezos' | 'Polkadot' | 'TRON' | 'Bitcoin Cash' | 'Litecoin' | 'Nervos' pub fn get_appname_by_instid(instid: &str) -> Option<&str> { @@ -18,6 +18,7 @@ pub fn get_appname_by_instid(instid: &str) -> Option<&str> { IMK_AID => Some("IMK"), NERVOS_AID => Some("Nervos"), TEZOS_AID => Some("Tezos"), + DOGECOIN_AID => Some("Dogecoin"), _ => None, } } @@ -36,6 +37,7 @@ pub fn get_instid_by_appname(appname: &str) -> Option<&str> { "Bitcoin Cash" => Some(BCH_AID), "Litecoin" => Some(LTC_AID), "IMK" => Some(IMK_AID), + "Dogecoin" => Some(DOGECOIN_AID), _ => None, } } @@ -56,6 +58,10 @@ mod tests { assert_eq!(get_appname_by_instid("695F626368").unwrap(), "Bitcoin Cash"); assert_eq!(get_appname_by_instid("695F6C7463").unwrap(), "Litecoin"); assert_eq!(get_appname_by_instid("695F696D6B").unwrap(), "IMK"); + assert_eq!( + get_appname_by_instid("695F646F6765636F696E").unwrap(), + "Dogecoin" + ); assert!(get_appname_by_instid("1111111111").is_none()); } @@ -72,6 +78,10 @@ mod tests { ); assert_eq!(get_instid_by_appname("Bitcoin Cash").unwrap(), "695F626368"); assert_eq!(get_instid_by_appname("Litecoin").unwrap(), "695F6C7463"); + assert_eq!( + get_instid_by_appname("Dogecoin").unwrap(), + "695F646F6765636F696E" + ); assert!(get_instid_by_appname("APPLET").is_none()); } } diff --git a/imkey-core/ikc-common/src/coin_info.rs b/imkey-core/ikc-common/src/coin_info.rs index 56973e19..1d6015d4 100644 --- a/imkey-core/ikc-common/src/coin_info.rs +++ b/imkey-core/ikc-common/src/coin_info.rs @@ -46,6 +46,34 @@ lazy_static! { network: "TESTNET".to_string(), seg_wit: "P2WPKH".to_string(), }, + CoinInfo { + coin: "BITCOIN".to_string(), + derivation_path: "m/84'/0'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "MAINNET".to_string(), + seg_wit: "VERSION_0".to_string(), + }, + CoinInfo { + coin: "BITCOIN".to_string(), + derivation_path: "m/84'/1'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "TESTNET".to_string(), + seg_wit: "VERSION_0".to_string(), + }, + CoinInfo { + coin: "BITCOIN".to_string(), + derivation_path: "m/86'/0'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "MAINNET".to_string(), + seg_wit: "VERSION_1".to_string(), + }, + CoinInfo { + coin: "BITCOIN".to_string(), + derivation_path: "m/86'/1'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "TESTNET".to_string(), + seg_wit: "VERSION_1".to_string(), + }, CoinInfo { coin: "BITCOINCASH".to_string(), derivation_path: "m/44'/145'/0'/0/0".to_string(), @@ -207,6 +235,62 @@ lazy_static! { network: "TESTNET".to_string(), seg_wit: "".to_string(), }, + CoinInfo { + coin: "DOGECOIN".to_string(), + derivation_path: "m/44'/3'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "MAINNET".to_string(), + seg_wit: "NONE".to_string(), + }, + CoinInfo { + coin: "DOGECOIN".to_string(), + derivation_path: "m/44'/1'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "TESTNET".to_string(), + seg_wit: "NONE".to_string(), + }, + CoinInfo { + coin: "DOGECOIN".to_string(), + derivation_path: "m/49'/3'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "MAINNET".to_string(), + seg_wit: "P2WPKH".to_string(), + }, + CoinInfo { + coin: "DOGECOIN".to_string(), + derivation_path: "m/49'/1'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "TESTNET".to_string(), + seg_wit: "P2WPKH".to_string(), + }, + CoinInfo { + coin: "DOGECOIN".to_string(), + derivation_path: "m/84'/3'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "MAINNET".to_string(), + seg_wit: "VERSION_0".to_string(), + }, + CoinInfo { + coin: "DOGECOIN".to_string(), + derivation_path: "m/84'/1'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "TESTNET".to_string(), + seg_wit: "VERSION_0".to_string(), + }, + CoinInfo { + coin: "DOGECOIN".to_string(), + derivation_path: "m/86'/3'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "MAINNET".to_string(), + seg_wit: "VERSION_1".to_string(), + }, + CoinInfo { + coin: "DOGECOIN".to_string(), + derivation_path: "m/86'/1'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "TESTNET".to_string(), + seg_wit: "VERSION_1".to_string(), + }, ]; RwLock::new(coin_infos) }; diff --git a/imkey-core/ikc-common/src/common.rs b/imkey-core/ikc-common/src/common.rs index f1333867..1bf6a748 100644 --- a/imkey-core/ikc-common/src/common.rs +++ b/imkey-core/ikc-common/src/common.rs @@ -17,4 +17,6 @@ pub struct SignParam { pub sender: ::prost::alloc::string::String, #[prost(string, tag = "8")] pub fee: ::prost::alloc::string::String, + #[prost(string, tag = "9")] + pub seg_wit: ::prost::alloc::string::String, } diff --git a/imkey-core/ikc-common/src/constants.rs b/imkey-core/ikc-common/src/constants.rs index 37046393..a431563e 100644 --- a/imkey-core/ikc-common/src/constants.rs +++ b/imkey-core/ikc-common/src/constants.rs @@ -1,6 +1,6 @@ -pub const VERSION: &str = "2.15.2"; -// pub const URL: &str = "https://imkey.online:1000/imkey"; -pub const URL: &str = "https://imkeyserver.com:10444/imkey"; +pub const VERSION: &str = "2.16.0"; +pub const URL: &str = "https://imkey.online:1000/imkey"; +// pub const URL: &str = "https://imkeyserver.com:10444/imkey"; pub const TSM_ACTION_SE_SECURE_CHECK: &str = "/seSecureCheck"; pub const TSM_ACTION_APP_DOWNLOAD: &str = "/appDownload"; @@ -29,6 +29,7 @@ pub const NERVOS_AID: &str = "695F6B315F636B62"; pub const TEZOS_AID: &str = "695F65645F78747A"; pub const BCH_AID: &str = "695F626368"; pub const LTC_AID: &str = "695F6C7463"; +pub const DOGECOIN_AID: &str = "695F646F6765636F696E"; pub const BL_AID: &str = "D0426F6F746C6F61646572"; @@ -118,6 +119,13 @@ pub const ETH_TRANSACTION_TYPE_EIP1559: &str = "02"; pub const ETH_MAX_SUPPORT_PAYMENT_LEN: usize = 255; +pub const BTC_PSBT_TRX_PER_PAGE_NUMBER: usize = 3; + +pub const BTC_SEG_WIT_TYPE_LEGACY: &str = "NONE"; +pub const BTC_SEG_WIT_TYPE_P2WPKH: &str = "P2WPKH"; +pub const BTC_SEG_WIT_TYPE_VERSION_0: &str = "VERSION_0"; +pub const BTC_SEG_WIT_TYPE_VERSION_1: &str = "VERSION_1"; + lazy_static! { /// Lazily initialized secp256k1 engine pub static ref SECP256K1_ENGINE: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); diff --git a/imkey-core/ikc-common/src/error.rs b/imkey-core/ikc-common/src/error.rs index 6f9611c8..7015e742 100644 --- a/imkey-core/ikc-common/src/error.rs +++ b/imkey-core/ikc-common/src/error.rs @@ -8,6 +8,8 @@ pub enum CommonError { InvalidKeyIvLength, #[error("invalid_base58")] InvalidBase58, + #[error("missing_network")] + MissingNetwork, } #[derive(Error, Debug, PartialOrd, PartialEq)] @@ -40,6 +42,8 @@ pub enum ApduError { ImkeyInMenuPage, #[error("imkey_pin_not_verified")] ImkeyPinNotVerified, + #[error("imkey_mnemonic_check_fail")] + ImkeyMnemonicCheckFail, } #[derive(Error, Debug, PartialOrd, PartialEq)] @@ -90,4 +94,6 @@ pub enum CoinError { InvalidAddrLength, #[error("invalid_utxo")] InvalidUtxo, + #[error("missing_signature")] + MissingSignature, } diff --git a/imkey-core/ikc-common/src/hex.rs b/imkey-core/ikc-common/src/hex.rs new file mode 100644 index 00000000..f8ac1a77 --- /dev/null +++ b/imkey-core/ikc-common/src/hex.rs @@ -0,0 +1,124 @@ +use super::Result; +use anyhow::anyhow; + +pub trait ToHex { + fn to_hex(&self) -> String; + + fn to_0x_hex(&self) -> String { + format!("0x{}", self.to_hex()) + } +} + +pub trait FromHex +where + Self: Sized, +{ + fn from_hex>(value: T) -> Result; + + fn from_0x_hex>(value: T) -> Result { + if value.as_ref().len() == 0 { + return Ok(Self::from_hex("")?); + } + + let bytes = value.as_ref(); + Self::from_hex(&bytes[2..bytes.len()]) + } + + fn from_hex_auto>(value: T) -> Result { + let bytes = value.as_ref(); + if bytes.len() >= 2 && bytes[0] == b'0' && (bytes[1] == b'x' || bytes[1] == b'X') { + Self::from_0x_hex(value) + } else { + Self::from_hex(value) + } + } +} + +impl> ToHex for T { + fn to_hex(&self) -> String { + hex::encode(self) + } +} + +impl ToHex for [u8] { + fn to_hex(&self) -> String { + hex::encode(self) + } +} + +impl FromHex for Vec { + fn from_hex>(value: T) -> Result { + hex::decode(value).map_err(|e| anyhow!("{}", e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::{FromHex, ToHex}; + #[test] + fn test_to_hex() { + let data = vec![0x01, 0x02, 0x03, 0x04]; + assert_eq!(data.to_hex(), "01020304"); + } + + #[test] + fn test_to_hex_empty() { + let data = vec![]; + assert_eq!(data.to_hex(), ""); + } + + #[test] + fn test_to_hex_with_prefix_empty() { + let data = vec![]; + assert_eq!(data.to_0x_hex(), "0x"); + } + + #[test] + fn test_to_hex_from_slice() { + let data = vec![0x01, 0x02, 0x03, 0x04]; + assert_eq!(data[0..2].to_hex(), "0102"); + } + + #[test] + fn test_to_hex_from_fixed_array() { + let data = [0x01, 0x02, 0x03, 0x04]; + assert_eq!(data.to_hex(), "01020304"); + } + + #[test] + fn test_to_hex_with_prefix() { + let data = vec![0x01, 0x02, 0x03, 0x04]; + assert_eq!(data.to_0x_hex(), "0x01020304"); + } + + #[test] + fn test_from_hex() { + let data = vec![0x01, 0x02, 0x03, 0x04]; + assert_eq!(Vec::from_hex("01020304").unwrap(), data); + } + + #[test] + fn test_from_hex_with_prefix() { + let value = "0x01020304"; + assert_eq!(Vec::from_0x_hex(value).unwrap(), [0x01, 0x02, 0x03, 0x04]); + } + + #[test] + fn test_from_hex_with_prefix_error() { + let value = "0x010203041"; + assert!(Vec::from_0x_hex(value).is_err(),); + } + + #[test] + fn test_from_hex_auto() { + assert_eq!( + Vec::from_hex_auto("0x01020304").unwrap(), + [0x01, 0x02, 0x03, 0x04] + ); + + assert_eq!( + Vec::from_hex_auto("01020304").unwrap(), + [0x01, 0x02, 0x03, 0x04] + ); + } +} diff --git a/imkey-core/ikc-common/src/lib.rs b/imkey-core/ikc-common/src/lib.rs index 6c0d1e3a..209404d5 100644 --- a/imkey-core/ikc-common/src/lib.rs +++ b/imkey-core/ikc-common/src/lib.rs @@ -6,6 +6,7 @@ pub mod common; pub mod constants; pub mod curve; pub mod error; +pub mod hex; pub mod https; pub mod path; pub mod utility; diff --git a/imkey-core/ikc-common/src/utility.rs b/imkey-core/ikc-common/src/utility.rs index af52b25f..a6938941 100644 --- a/imkey-core/ikc-common/src/utility.rs +++ b/imkey-core/ikc-common/src/utility.rs @@ -1,6 +1,7 @@ use crate::aes::cbc::encrypt_pkcs7; use crate::constants::SECP256K1_ENGINE; use crate::error::CommonError; +use crate::hex::FromHex; use crate::Result; use bitcoin::hashes::{sha256, Hash}; use bitcoin::util::base58; @@ -206,6 +207,19 @@ pub fn network_convert(network: &str) -> Network { } } +pub fn utf8_or_hex_to_bytes(value: &str) -> Result> { + if value.to_lowercase().starts_with("0x") { + let ret = FromHex::from_0x_hex(value); + if ret.is_err() { + Ok(value.as_bytes().to_vec()) + } else { + ret + } + } else { + Ok(value.as_bytes().to_vec()) + } +} + #[cfg(test)] mod tests { use crate::utility; diff --git a/imkey-core/ikc-device/src/device_binding.rs b/imkey-core/ikc-device/src/device_binding.rs index 5742e075..ca2df55d 100644 --- a/imkey-core/ikc-device/src/device_binding.rs +++ b/imkey-core/ikc-device/src/device_binding.rs @@ -237,7 +237,7 @@ pub fn bind_test() { // pub const TEST_KEY_PATH: &str = "/tmp/"; // pub const TEST_BIND_CODE: &str = "MCYNK5AH"; pub const TEST_KEY_PATH: &str = "/tmp/"; -pub const TEST_BIND_CODE: &str = "FT2Z3LT2"; +pub const TEST_BIND_CODE: &str = "CM3SH5QE"; #[cfg(test)] mod test { diff --git a/imkey-core/ikc-device/src/device_manager.rs b/imkey-core/ikc-device/src/device_manager.rs index 344bd913..bbc0069f 100644 --- a/imkey-core/ikc-device/src/device_manager.rs +++ b/imkey-core/ikc-device/src/device_manager.rs @@ -224,10 +224,20 @@ pub fn is_bl_status() -> Result { Ok(true) } +pub fn get_btc_apple_version() -> Result { + select_isd()?; + let res = send_apdu("00a4040005695f62746300".to_string())?; + ApduCheck::check_response(res.as_str())?; + let btc_version = hex::decode(&res[0..(res.len() - 4)])?; + let btc_version = String::from_utf8(btc_version)?; + Ok(btc_version) +} + #[cfg(test)] mod test { use crate::device_manager::{ - active_device, app_delete, app_download, app_update, bind_check, is_bl_status, + active_device, app_delete, app_download, app_update, bind_check, get_btc_apple_version, + is_bl_status, }; use ikc_common::constants; use ikc_transport::hid_api::hid_connect; @@ -296,4 +306,11 @@ mod test { let result = active_device(); assert!(result.is_ok()); } + + #[test] + fn get_btc_version_test() { + assert!(hid_connect(constants::DEVICE_MODEL_NAME).is_ok()); + let result = get_btc_apple_version(); + assert!(result.is_ok()); + } } diff --git a/imkey-core/ikc-proto/src/api.proto b/imkey-core/ikc-proto/src/api.proto index b1ac8b16..f382735c 100644 --- a/imkey-core/ikc-proto/src/api.proto +++ b/imkey-core/ikc-proto/src/api.proto @@ -41,7 +41,6 @@ message PubKeyParam { string chainType = 1; string path = 2; string network = 3; - string isSegWit = 4; } message PubKeyResult { @@ -110,7 +109,6 @@ message DeriveAccountsParam { string segWit = 4; string chainId = 5; string curve = 6; - string bech32Prefix = 7; } repeated Derivation derivations= 1; } diff --git a/imkey-core/ikc-proto/src/btc.proto b/imkey-core/ikc-proto/src/btc.proto index 9c12d383..df162078 100644 --- a/imkey-core/ikc-proto/src/btc.proto +++ b/imkey-core/ikc-proto/src/btc.proto @@ -32,3 +32,20 @@ message BtcTxOutput { string txHash = 2; string wtxHash = 3; } + +message PsbtInput { + string psbt = 1; + bool autoFinalize = 2; +} + +message PsbtOutput { + string psbt = 1; +} + +message BtcMessageInput { + string message = 1; +} + +message BtcMessageOutput { + string signature = 1; +} diff --git a/imkey-core/ikc-proto/src/common.proto b/imkey-core/ikc-proto/src/common.proto index ec52cb75..71b2dd89 100644 --- a/imkey-core/ikc-proto/src/common.proto +++ b/imkey-core/ikc-proto/src/common.proto @@ -11,4 +11,5 @@ message SignParam { string receiver = 6; string sender = 7; string fee = 8; + string segWit = 9; } \ No newline at end of file diff --git a/imkey-core/ikc-proto/src/tron.proto b/imkey-core/ikc-proto/src/tron.proto index 6c9c7448..c6f92a38 100644 --- a/imkey-core/ikc-proto/src/tron.proto +++ b/imkey-core/ikc-proto/src/tron.proto @@ -10,9 +10,9 @@ message TronTxOutput { } message TronMessageInput { - string message = 2; - bool is_hex =4; - bool is_tron_header=5; + string message = 1; + string header = 2;//"TRON","ETH","NONE" + uint32 version = 3;//1: V1 2:V2 } message TronMessageOutput { diff --git a/imkey-core/ikc-wallet/coin-bch/src/common.rs b/imkey-core/ikc-wallet/coin-bch/src/common.rs index 716682ec..7383bffe 100644 --- a/imkey-core/ikc-wallet/coin-bch/src/common.rs +++ b/imkey-core/ikc-wallet/coin-bch/src/common.rs @@ -2,7 +2,6 @@ use crate::address::BchAddress; use crate::transaction::Utxo; use crate::Result; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::Secp256k1 as BitcoinSecp256k1; use bitcoin::util::base58; use bitcoin::util::bip32::{ChainCode, ChildNumber, ExtendedPubKey}; use bitcoin::{Address, PublicKey}; diff --git a/imkey-core/ikc-wallet/coin-bitcoin/Cargo.toml b/imkey-core/ikc-wallet/coin-bitcoin/Cargo.toml index 208aafa0..b101a4d8 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/Cargo.toml +++ b/imkey-core/ikc-wallet/coin-bitcoin/Cargo.toml @@ -18,4 +18,5 @@ num-bigint = "=0.4.3" anyhow = "=1.0.79" bytes = "=1.4.0" prost = "=0.11.2" -prost-types = "=0.11.2" \ No newline at end of file +prost-types = "=0.11.2" +bech32 = "=0.9.1" \ No newline at end of file diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/address.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/address.rs index c91543a2..9ec4ceaa 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/src/address.rs +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/address.rs @@ -4,8 +4,8 @@ use bitcoin::network::constants::Network; use bitcoin::schnorr::UntweakedPublicKey; use bitcoin::util::bip32::{ChainCode, ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint}; use bitcoin::{Address, PublicKey}; -use bitcoin_hashes::{hash160, Hash}; use ikc_common::apdu::{ApduCheck, BtcApdu, CoinCommonApdu}; +use ikc_common::constants; use ikc_common::error::CommonError; use ikc_common::path::check_path_validity; use ikc_common::utility::hex_to_bytes; @@ -147,9 +147,9 @@ impl BtcAddress { check_path_validity(path)?; let address = match seg_wit { - "P2WPKH" => Self::p2shwpkh(network, path)?, - "VERSION_0" => Self::p2wpkh(network, path)?, - "VERSION_1" => Self::p2tr(network, path)?, + constants::BTC_SEG_WIT_TYPE_P2WPKH => Self::p2shwpkh(network, path)?, + constants::BTC_SEG_WIT_TYPE_VERSION_0 => Self::p2wpkh(network, path)?, + constants::BTC_SEG_WIT_TYPE_VERSION_1 => Self::p2tr(network, path)?, _ => Self::p2pkh(network, path)?, }; @@ -158,23 +158,17 @@ impl BtcAddress { Ok(address) } - // pub fn display_segwit_address(network: Network, path: &str) -> Result { - // check_path_validity(path)?; - // let address_str = Self::p2shwpkh(network, path)?; - // let apdu_res = send_apdu(BtcApdu::register_address( - // &address_str.clone().into_bytes().to_vec(), - // ))?; - // ApduCheck::check_response(apdu_res.as_str())?; - // Ok(address_str) - // } - pub fn from_public_key(public_key: &str, network: Network, seg_wit: &str) -> Result { let mut pub_key_obj = PublicKey::from_str(public_key)?; pub_key_obj.compressed = true; let address = match seg_wit { - "P2WPKH" => Address::p2shwpkh(&pub_key_obj, network)?.to_string(), - "VERSION_0" => Address::p2wpkh(&pub_key_obj, network)?.to_string(), - "VERSION_1" => { + constants::BTC_SEG_WIT_TYPE_P2WPKH => { + Address::p2shwpkh(&pub_key_obj, network)?.to_string() + } + constants::BTC_SEG_WIT_TYPE_VERSION_0 => { + Address::p2wpkh(&pub_key_obj, network)?.to_string() + } + constants::BTC_SEG_WIT_TYPE_VERSION_1 => { let untweak_pub_key = UntweakedPublicKey::from(secp256k1::PublicKey::from_slice( &hex_to_bytes(&public_key)?, )?); @@ -323,4 +317,31 @@ mod test { let segwit_address = result.ok().unwrap(); assert_eq!("37E2J9ViM4QFiewo7aw5L3drF2QKB99F9e", segwit_address); } + + #[test] + fn display_native_segwit_address_test() { + bind_test(); + let network: Network = Network::Bitcoin; + let path: &str = "m/84'/0'/0'"; + let result = BtcAddress::display_address(network, path, "VERSION_0"); + + assert!(result.is_ok()); + let segwit_address = result.ok().unwrap(); + assert_eq!("bc1qhuwav68m49d8epty9ztg8yag7ku27jfccyz3hp", segwit_address); + } + + #[test] + fn display_taproot_address_test() { + bind_test(); + let network: Network = Network::Bitcoin; + let path: &str = "m/86'/0'/0'"; + let result = BtcAddress::display_address(network, path, "VERSION_1"); + + assert!(result.is_ok()); + let segwit_address = result.ok().unwrap(); + assert_eq!( + "bc1p26r56upnktz0qm4vxw3228v956rxsc4sevasswxdvh9ysnq509fqctph3w", + segwit_address + ); + } } diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/btc_kin_address.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/btc_kin_address.rs new file mode 100644 index 00000000..ae3ff515 --- /dev/null +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/btc_kin_address.rs @@ -0,0 +1,524 @@ +use crate::common::get_xpub_data; +use crate::network::BtcKinNetwork; +use crate::Result; +use bitcoin::hash_types::PubkeyHash as PubkeyHashType; +use bitcoin::hash_types::ScriptHash as ScriptHashType; +use bitcoin::network::constants::Network; +use bitcoin::schnorr::UntweakedPublicKey; +use bitcoin::util::address::Payload; +use bitcoin::util::address::{Error as LibAddressError, WitnessVersion}; +use bitcoin::util::base58; +use bitcoin::util::bip32::{ChainCode, ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint}; +use bitcoin::{Address as LibAddress, PublicKey, Script}; +use bitcoin_hashes::Hash; +use core::result; +use ikc_common::apdu::{ApduCheck, BtcApdu, CoinCommonApdu}; +use ikc_common::coin_info::CoinInfo; +use ikc_common::constants; +use ikc_common::error::CoinError; +use ikc_common::path::check_path_validity; +use ikc_common::path::get_parent_path; +use ikc_common::utility::hex_to_bytes; +use ikc_transport::message::send_apdu; +use secp256k1::{PublicKey as Secp256k1PublicKey, Secp256k1}; +use std::convert::TryFrom; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +pub trait AddressTrait { + fn from_public_key( + public_key: &str, + network: &BtcKinNetwork, + seg_wit: &str, + ) -> Result; + + fn is_valid(address: &str, coin: &CoinInfo) -> bool; +} + +pub trait ScriptPubkey { + fn script_pubkey(&self) -> Script; +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BtcKinAddress { + pub network: BtcKinNetwork, + pub payload: Payload, +} + +impl AddressTrait for BtcKinAddress { + fn from_public_key( + public_key: &str, + network: &BtcKinNetwork, + seg_wit: &str, + ) -> Result { + let mut pub_key_obj = PublicKey::from_str(public_key)?; + pub_key_obj.compressed = true; + + let address = match seg_wit { + constants::BTC_SEG_WIT_TYPE_P2WPKH => { + LibAddress::p2shwpkh(&pub_key_obj, Network::Bitcoin)? + } + constants::BTC_SEG_WIT_TYPE_VERSION_0 => { + LibAddress::p2wpkh(&pub_key_obj, Network::Bitcoin)? + } + constants::BTC_SEG_WIT_TYPE_VERSION_1 => { + let untweak_pub_key = UntweakedPublicKey::from(secp256k1::PublicKey::from_slice( + &hex_to_bytes(&public_key)?, + )?); + let secp256k1 = Secp256k1::new(); + LibAddress::p2tr(&secp256k1, untweak_pub_key, None, Network::Bitcoin) + } + _ => LibAddress::p2pkh(&pub_key_obj, Network::Bitcoin), + }; + + Ok(BtcKinAddress { + payload: address.payload, + network: network.clone(), + }) + } + + fn is_valid(address: &str, coin: &CoinInfo) -> bool { + let ret = BtcKinAddress::from_str(address); + if let Ok(btc_kin_addr) = ret { + btc_kin_addr.network.network == coin.network + } else { + false + } + } +} + +impl BtcKinAddress { + pub fn p2pkh(network: &BtcKinNetwork, path: &str) -> Result { + check_path_validity(path)?; + + let pub_key = &get_xpub_data(path, true)?[..130]; + + let mut pub_key_obj = PublicKey::from_str(pub_key)?; + pub_key_obj.compressed = true; + let addr = LibAddress::p2pkh(&pub_key_obj, Network::Bitcoin); + + Ok(BtcKinAddress { + payload: addr.payload, + network: network.clone(), + }) + } + + pub fn p2shwpkh(network: &BtcKinNetwork, path: &str) -> Result { + check_path_validity(path)?; + + let pub_key = &get_xpub_data(path, true)?[..130]; + + let mut pub_key_obj = PublicKey::from_str(pub_key)?; + pub_key_obj.compressed = true; + + let address = LibAddress::p2shwpkh(&pub_key_obj, Network::Bitcoin)?; + + Ok(BtcKinAddress { + payload: address.payload, + network: network.clone(), + }) + } + + pub fn p2wpkh(network: &BtcKinNetwork, path: &str) -> Result { + check_path_validity(path)?; + + let pub_key = &get_xpub_data(path, true)?[..130]; + + let mut pub_key_obj = PublicKey::from_str(pub_key)?; + pub_key_obj.compressed = true; + let addr = LibAddress::p2wpkh(&pub_key_obj, Network::Bitcoin)?; + Ok(BtcKinAddress { + payload: addr.payload, + network: network.clone(), + }) + } + + pub fn p2tr(network: &BtcKinNetwork, path: &str) -> Result { + check_path_validity(path)?; + + let pub_key = &get_xpub_data(path, true)?[..130]; + let untweak_pub_key = + UntweakedPublicKey::from(secp256k1::PublicKey::from_slice(&hex_to_bytes(&pub_key)?)?); + + let secp256k1 = Secp256k1::new(); + let addr = LibAddress::p2tr(&secp256k1, untweak_pub_key, None, Network::Bitcoin); + Ok(BtcKinAddress { + payload: addr.payload, + network: network.clone(), + }) + } + + pub fn display_address(network: &BtcKinNetwork, path: &str, seg_wit: &str) -> Result { + check_path_validity(path)?; + + let address = match seg_wit { + constants::BTC_SEG_WIT_TYPE_P2WPKH => Self::p2shwpkh(network, path)?, + constants::BTC_SEG_WIT_TYPE_VERSION_0 => Self::p2wpkh(network, path)?, + constants::BTC_SEG_WIT_TYPE_VERSION_1 => Self::p2tr(network, path)?, + _ => Self::p2pkh(network, path)?, + }; + + let apdu_res = send_apdu(BtcApdu::register_address(&address.to_string().as_bytes()))?; + ApduCheck::check_response(apdu_res.as_str())?; + Ok(address.to_string()) + } + + pub fn script_pubkey(&self) -> Script { + self.payload.script_pubkey() + } +} + +impl ScriptPubkey for BtcKinAddress { + fn script_pubkey(&self) -> Script { + self.script_pubkey() + } +} + +impl FromStr for BtcKinAddress { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + // try bech32 + let bech32_network = bech32_network(s); + if let Some(network) = bech32_network { + // decode as bech32 + let (_, payload, _) = bech32::decode(s)?; + if payload.is_empty() { + return Err(LibAddressError::EmptyBech32Payload.into()); + } + + // Get the script version and program (converted from 5-bit to 8-bit) + let (version, program): (bech32::u5, Vec) = { + let (v, p5) = payload.split_at(1); + (v[0], bech32::FromBase32::from_base32(p5)?) + }; + + // Generic segwit checks. + if version.to_u8() > 16 { + return Err(LibAddressError::InvalidWitnessVersion(version.to_u8()).into()); + } + if program.len() < 2 || program.len() > 40 { + return Err(LibAddressError::InvalidWitnessProgramLength(program.len()).into()); + } + + // Specific segwit v0 check. + if version.to_u8() == 0 && (program.len() != 20 && program.len() != 32) { + return Err(LibAddressError::InvalidSegwitV0ProgramLength(program.len()).into()); + } + let payload = Payload::WitnessProgram { + version: WitnessVersion::try_from(version.to_u8())?, + program, + }; + return Ok(BtcKinAddress { + payload, + network: network.clone(), + }); + } + + let data = decode_base58(s).map_err(|_| CoinError::InvalidAddress)?; + if let Some(network) = BtcKinNetwork::find_by_prefix(data[0]) { + if network.p2pkh_prefix == data[0] { + return Ok(BtcKinAddress { + network: network.clone(), + payload: Payload::PubkeyHash(PubkeyHashType::from_slice(&data[1..]).unwrap()), + }); + } else if network.p2sh_prefix == data[0] { + return Ok(BtcKinAddress { + network: network.clone(), + payload: Payload::ScriptHash(ScriptHashType::from_slice(&data[1..]).unwrap()), + }); + } + } + + Err(LibAddressError::UnrecognizedScript.into()) + } +} + +impl Display for BtcKinAddress { + fn fmt(&self, fmt: &mut Formatter) -> core::fmt::Result { + match self.payload { + Payload::PubkeyHash(ref hash) => { + let mut prefixed = [0; 21]; + prefixed[0] = self.network.p2pkh_prefix; + prefixed[1..].copy_from_slice(&hash[..]); + base58::check_encode_slice_to_fmt(fmt, &prefixed[..]) + } + Payload::ScriptHash(ref hash) => { + let mut prefixed = [0; 21]; + prefixed[0] = self.network.p2sh_prefix; + prefixed[1..].copy_from_slice(&hash[..]); + base58::check_encode_slice_to_fmt(fmt, &prefixed[..]) + } + Payload::WitnessProgram { + version, + program: ref prog, + } => { + let mut bech32_writer = bech32::Bech32Writer::new( + self.network.bech32_hrp, + version.bech32_variant(), + fmt, + )?; + bech32::WriteBase32::write_u5(&mut bech32_writer, version.into())?; + bech32::ToBase32::write_base32(&prog, &mut bech32_writer) + } + } + } +} + +fn bech32_network(bech32: &str) -> Option<&BtcKinNetwork> { + let bech32_prefix = bech32.rfind('1').map(|sep| bech32.split_at(sep).0); + + if bech32_prefix.is_some() { + let prefix = bech32_prefix.unwrap(); + if !prefix.is_empty() { + return BtcKinNetwork::find_by_hrp(prefix); + } + } + return None; +} + +fn decode_base58(addr: &str) -> result::Result, LibAddressError> { + // Base58 + if addr.len() > 50 { + return Err(LibAddressError::Base58(base58::Error::InvalidLength( + addr.len() * 11 / 15, + ))); + } + let data = base58::from_check(addr)?; + if data.len() != 21 { + Err(LibAddressError::Base58(base58::Error::InvalidLength( + data.len(), + ))) + } else { + Ok(data) + } +} + +pub struct ImkeyPublicKey(); + +impl ImkeyPublicKey { + pub fn get_xpub(network: Network, path: &str) -> Result { + check_path_validity(path)?; + + let xpub_data = get_xpub_data(path, true)?; + let xpub_data = &xpub_data[..194].to_string(); + + let pub_key = &xpub_data[..130]; + let chain_code = &xpub_data[130..]; + + let parent_xpub = get_xpub_data(get_parent_path(path)?, true)?; + let parent_xpub = &parent_xpub[..130].to_string(); + let parent_pub_key_obj = Secp256k1PublicKey::from_str(parent_xpub)?; + + let pub_key_obj = Secp256k1PublicKey::from_str(pub_key)?; + + let chain_code_obj = ChainCode::from(hex::decode(chain_code).unwrap().as_slice()); + let parent_ext_pub_key = ExtendedPubKey { + network, + depth: 0u8, + parent_fingerprint: Fingerprint::default(), + child_number: ChildNumber::from_normal_idx(0).unwrap(), + public_key: parent_pub_key_obj, + chain_code: chain_code_obj, + }; + let fingerprint_obj = parent_ext_pub_key.fingerprint(); + + //build extend public key obj + let chain_code_obj = ChainCode::from(hex::decode(chain_code).unwrap().as_slice()); + let chain_number_vec: Vec = DerivationPath::from_str(path)?.into(); + let extend_public_key = ExtendedPubKey { + network, + depth: chain_number_vec.len() as u8, + parent_fingerprint: fingerprint_obj, + child_number: *chain_number_vec.get(chain_number_vec.len() - 1).unwrap(), + public_key: pub_key_obj, + chain_code: chain_code_obj, + }; + Ok(extend_public_key.to_string()) + } + + pub fn get_pub_key(path: &str) -> Result { + check_path_validity(path)?; + + let xpub_data = get_xpub_data(path, true)?; + let pub_key = &xpub_data[..130]; + + Ok(pub_key.to_string()) + } +} + +#[cfg(test)] +mod test { + use crate::{ + btc_kin_address::{BtcKinAddress, ImkeyPublicKey}, + network::BtcKinNetwork, + }; + use bitcoin::Network; + use ikc_device::device_binding::bind_test; + + #[test] + fn get_xpub_test() { + bind_test(); + + let version: Network = Network::Bitcoin; + let path: &str = "m/44'/0'/0'/0/0"; + let get_xpub_result = ImkeyPublicKey::get_xpub(version, path); + assert!(get_xpub_result.is_ok()); + let xpub = get_xpub_result.ok().unwrap(); + assert_eq!("xpub6FuzpGNBc46EfvmcvECyqXjrzGcKErQgpQcpvhw1tiC5yXvi1jUkzudMpdg5AaguiFstdVR5ASDbSceBswKRy6cAhpTgozmgxMUayPDrLLX", xpub); + } + + #[test] + fn get_xpub_path_error_test() { + bind_test(); + + let version: Network = Network::Bitcoin; + let path: &str = "m/44'"; + let get_xpub_result = ImkeyPublicKey::get_xpub(version, path); + assert!(get_xpub_result.is_err()); + } + + #[test] + fn get_xpub_path_is_null_test() { + bind_test(); + + let version: Network = Network::Bitcoin; + let path: &str = ""; + let get_xpub_result = ImkeyPublicKey::get_xpub(version, path); + assert!(get_xpub_result.is_err()); + } + + #[test] + fn p2pkh_test() { + bind_test(); + + let network = BtcKinNetwork::find_by_coin("BITCOIN", "MAINNET").unwrap(); + let path: &str = "m/44'/0'/0'/0/0"; + let btc_address = BtcKinAddress::p2pkh(network, path).unwrap().to_string(); + + assert_eq!("12z6UzsA3tjpaeuvA2Zr9jwx19Azz74D6g", btc_address); + } + + #[test] + fn p2shwpkh_address_test() { + bind_test(); + + let network = BtcKinNetwork::find_by_coin("BITCOIN", "MAINNET").unwrap(); + let path: &str = "m/49'/0'/0'/0/22"; + let segwit_address = BtcKinAddress::p2shwpkh(network, path).unwrap().to_string(); + + assert_eq!("37E2J9ViM4QFiewo7aw5L3drF2QKB99F9e", segwit_address); + } + #[test] + fn p2wpkh_address_test() { + bind_test(); + + let network = BtcKinNetwork::find_by_coin("BITCOIN", "MAINNET").unwrap(); + let path: &str = "m/49'/0'/0'/0/22"; + let address = BtcKinAddress::p2wpkh(network, path).unwrap().to_string(); + + assert_eq!("bc1qe74h3vkdcj94uph4wdpk48nlqjdy42y87mdm7q", address); + } + + #[test] + fn p2tr_address_test() { + bind_test(); + + let network = BtcKinNetwork::find_by_coin("BITCOIN", "MAINNET").unwrap(); + let path: &str = "m/49'/0'/0'/0/22"; + let address = BtcKinAddress::p2tr(network, path).unwrap().to_string(); + + assert_eq!( + "bc1ph40wj9vl3kwhxq747wxkcr63e4r3uaryagpetnkey4zqhucmjfzse24jrd", + address + ); + } + + #[test] + fn display_address_test() { + bind_test(); + let network = BtcKinNetwork::find_by_coin("BITCOIN", "MAINNET").unwrap(); + let path: &str = "m/44'/0'/0'/0/0"; + let address = BtcKinAddress::display_address(network, path, "NONE").unwrap(); + + assert_eq!("12z6UzsA3tjpaeuvA2Zr9jwx19Azz74D6g", address); + } + + #[test] + fn display_segwit_address_test() { + bind_test(); + let network = BtcKinNetwork::find_by_coin("BITCOIN", "MAINNET").unwrap(); + let path: &str = "m/49'/0'/0'/0/22"; + let address = BtcKinAddress::display_address(network, path, "P2WPKH").unwrap(); + + assert_eq!("37E2J9ViM4QFiewo7aw5L3drF2QKB99F9e", address); + } + + #[test] + fn display_native_segwit_address_test() { + bind_test(); + let network = BtcKinNetwork::find_by_coin("BITCOIN", "MAINNET").unwrap(); + let path: &str = "m/84'/0'/0'"; + let address = BtcKinAddress::display_address(network, path, "VERSION_0").unwrap(); + + assert_eq!("bc1qhuwav68m49d8epty9ztg8yag7ku27jfccyz3hp", address); + } + + #[test] + fn display_taproot_address_test() { + bind_test(); + let network = BtcKinNetwork::find_by_coin("BITCOIN", "MAINNET").unwrap(); + let path: &str = "m/86'/0'/0'"; + let address = BtcKinAddress::display_address(network, path, "VERSION_1").unwrap(); + + assert_eq!( + "bc1p26r56upnktz0qm4vxw3228v956rxsc4sevasswxdvh9ysnq509fqctph3w", + address + ); + } + + #[test] + fn test_dogecoin_address() { + bind_test(); + + let network = BtcKinNetwork::find_by_coin("DOGECOIN", "MAINNET").unwrap(); + let path: &str = "m/44'/3'/0'/0/0"; + let address = BtcKinAddress::p2pkh(network, path).unwrap().to_string(); + assert_eq!("DQ4tVEqdPWHc1aVBm4Sfwft8XyNRPMEchR", address); + + let path: &str = "m/44'/1'/0'/0/0"; + let address = BtcKinAddress::p2shwpkh(network, path).unwrap().to_string(); + assert_eq!("A6tT5rU6MZBzArAVVei5PqqocfxBqJhSqg", address); + + let path: &str = "m/44'/1'/0'/0/0"; + let address = BtcKinAddress::p2wpkh(network, path).unwrap().to_string(); + assert_eq!("1q8qlms89s5078yj67pr8ch02qgvmdwy0k24vwhn", address); + + let path: &str = "m/44'/1'/0'/0/0"; + let address = BtcKinAddress::p2tr(network, path).unwrap().to_string(); + assert_eq!( + "1pd2gajgcpr7c5ajgl377sgmqexw5jxqvl305zw2a7aeujf8pun7ksh45tuj", + address + ); + + let network = BtcKinNetwork::find_by_coin("DOGECOIN", "TESTNET").unwrap(); + let path: &str = "m/44'/3'/0'/0/0"; + let address = BtcKinAddress::p2pkh(network, path).unwrap().to_string(); + assert_eq!("no7xDFaYKUkKtZ4Nnt68C5URmqkiMUTRTE", address); + + let path: &str = "m/44'/1'/0'/0/0"; + let address = BtcKinAddress::p2shwpkh(network, path).unwrap().to_string(); + assert_eq!("2N7hQQkLDtwpSUGRZkefXmfCh8SnKabUcC5", address); + + let path: &str = "m/44'/1'/0'/0/0"; + let address = BtcKinAddress::p2wpkh(network, path).unwrap().to_string(); + assert_eq!("1q8qlms89s5078yj67pr8ch02qgvmdwy0k24vwhn", address); + + let path: &str = "m/44'/1'/0'/0/0"; + let address = BtcKinAddress::p2tr(network, path).unwrap().to_string(); + assert_eq!( + "1pd2gajgcpr7c5ajgl377sgmqexw5jxqvl305zw2a7aeujf8pun7ksh45tuj", + address + ); + } +} diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/btcapi.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/btcapi.rs index 0217ac6e..5133d768 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/src/btcapi.rs +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/btcapi.rs @@ -56,3 +56,29 @@ pub struct BtcTxOutput { #[prost(string, tag = "3")] pub wtx_hash: ::prost::alloc::string::String, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PsbtInput { + #[prost(string, tag = "1")] + pub psbt: ::prost::alloc::string::String, + #[prost(bool, tag = "2")] + pub auto_finalize: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PsbtOutput { + #[prost(string, tag = "1")] + pub psbt: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BtcMessageInput { + #[prost(string, tag = "1")] + pub message: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BtcMessageOutput { + #[prost(string, tag = "1")] + pub signature: ::prost::alloc::string::String, +} diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/common.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/common.rs index 50aeb447..01ca8ece 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/src/common.rs +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/common.rs @@ -1,57 +1,28 @@ use crate::transaction::Utxo; use crate::Result; -use bitcoin::schnorr::UntweakedPublicKey; use bitcoin::util::base58; -use bitcoin::util::bip32::{ChainCode, ChildNumber, ExtendedPubKey}; -use bitcoin::{Address, AddressType, Network, PublicKey}; +use bitcoin::{Network, PublicKey}; use ikc_common::apdu::{ApduCheck, BtcApdu, CoinCommonApdu}; use ikc_common::error::CoinError; -use ikc_common::utility::{hex_to_bytes, sha256_hash}; +use ikc_common::utility::sha256_hash; use ikc_transport::message::send_apdu; use secp256k1::{ecdsa::Signature, Message, PublicKey as Secp256k1PublicKey, Secp256k1}; use std::str::FromStr; /** -utxo address verify +get utxo public key */ -pub fn address_verify(utxos: &Vec, network: Network) -> Result> { +pub fn get_utxo_pub_key(utxos: &Vec) -> Result> { let mut utxo_pub_key_vec: Vec = vec![]; for utxo in utxos { let xpub_data = get_xpub_data(&utxo.derive_path, false)?; //parsing xpub data let derive_pub_key = &xpub_data[..130]; - let chain_code = &xpub_data[130..194]; - let mut extend_public_key = ExtendedPubKey { - network, - depth: 0, - parent_fingerprint: Default::default(), - child_number: ChildNumber::from_normal_idx(0)?, - public_key: Secp256k1PublicKey::from_str(derive_pub_key)?, - chain_code: ChainCode::from(hex_to_bytes(chain_code)?.as_slice()), - }; - let public_key = PublicKey::from_str(&extend_public_key.public_key.to_string())?; - let script_pubkey = utxo.address.script_pubkey(); - let mut se_gen_address = "".to_string(); - if script_pubkey.is_p2pkh() { - se_gen_address = Address::p2pkh(&public_key, network).to_string(); - } else if script_pubkey.is_p2sh() { - se_gen_address = Address::p2shwpkh(&public_key, network)?.to_string(); - } else if script_pubkey.is_v0_p2wpkh() { - se_gen_address = Address::p2wpkh(&public_key, network)?.to_string(); - } else if script_pubkey.is_v1_p2tr() { - let untweak_pub_key = UntweakedPublicKey::from(secp256k1::PublicKey::from_slice( - &hex_to_bytes(&derive_pub_key)?, - )?); - let secp256k1 = Secp256k1::new(); - se_gen_address = Address::p2tr(&secp256k1, untweak_pub_key, None, network).to_string(); - } else { - return Err(CoinError::InvalidAddress.into()); - }; - if !se_gen_address.eq(&utxo.address.to_string()) { - return Err(CoinError::ImkeyAddressMismatchWithPath.into()); - } - utxo_pub_key_vec.push(extend_public_key.public_key.to_string()); + let mut public_key = PublicKey::from_str(derive_pub_key)?; + public_key.compressed = true; + + utxo_pub_key_vec.push(public_key.to_string()); } Ok(utxo_pub_key_vec) } @@ -67,6 +38,15 @@ pub fn get_xpub_data(path: &str, verify_flag: bool) -> Result { Ok(xpub_data) } +/** +select btc applet + */ +pub fn select_btc_applet() -> Result<()> { + let select_response = send_apdu(BtcApdu::select_applet())?; + ApduCheck::check_response(&select_response)?; + Ok(()) +} + /** sign verify */ @@ -92,7 +72,11 @@ get address version pub fn get_address_version(network: Network, address: &str) -> Result { let version = match network { Network::Bitcoin => { - if address.starts_with('1') || address.starts_with('3') { + if address.starts_with('1') + || address.starts_with('3') + || address.starts_with('D') + || address.starts_with('A') + { let address_bytes = base58::from(address)?; address_bytes.as_slice()[0] } else if address.starts_with("bc1") { diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/lib.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/lib.rs index b1e432af..9ccbbac7 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/src/lib.rs +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/lib.rs @@ -1,8 +1,13 @@ pub mod address; +pub mod btc_kin_address; pub mod btcapi; pub mod common; +pub mod message; +pub mod network; +pub mod psbt; pub mod transaction; pub mod usdt_transaction; + extern crate anyhow; use core::result; pub type Result = result::Result; diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/message.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/message.rs new file mode 100644 index 00000000..c749b463 --- /dev/null +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/message.rs @@ -0,0 +1,250 @@ +use crate::address::BtcAddress; +use crate::btcapi::{BtcMessageInput, BtcMessageOutput}; +use crate::common::select_btc_applet; +use crate::psbt::PsbtSigner; +use crate::Result; +use bitcoin::psbt::PartiallySignedTransaction; +use bitcoin::{ + Address, OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Txid, Witness, +}; +use bitcoin_hashes::hex::ToHex; +use hex::FromHex; +use ikc_common::error::CoinError; +use ikc_common::utility::{network_convert, sha256_hash, utf8_or_hex_to_bytes}; +use std::str::FromStr; + +pub struct MessageSinger { + pub derivation_path: String, + pub chain_type: String, + pub network: String, + pub seg_wit: String, +} +impl MessageSinger { + pub fn sign_message(&self, input: BtcMessageInput) -> Result { + let data = utf8_or_hex_to_bytes(&input.message)?; + + let path = format!("{}/0/0", self.derivation_path); + + let pub_key = BtcAddress::get_pub_key(&path)?; + + let network = network_convert(&self.network); + let address = BtcAddress::from_public_key(&pub_key, network, &self.seg_wit)?; + let script_pubkey = Address::from_str(&address)?.script_pubkey(); + let tx_id = get_spend_tx_id(&data, script_pubkey.clone())?; + + select_btc_applet()?; + + let mut psbt = create_to_sign_empty(tx_id, script_pubkey)?; + let mut psbt_signer = + PsbtSigner::new(&mut psbt, &self.derivation_path, true, network, true)?; + + psbt_signer.prevouts()?; + + let pub_keys = psbt_signer.get_pub_key()?; + + psbt_signer.calc_tx_hash()?; + + psbt_signer.get_preview_info()?; + + psbt_signer.tx_preview(network)?; + + psbt_signer.sign(&pub_keys)?; + + if let Some(witness) = &psbt.inputs[0].final_script_witness { + Ok(BtcMessageOutput { + signature: witness_to_vec(witness.to_vec()).to_hex(), + }) + } else { + if let Some(script_sig) = &psbt.inputs[0].final_script_sig { + Ok(BtcMessageOutput { + signature: format!("02{}", script_sig.to_hex()), + }) + } else { + Err(CoinError::MissingSignature.into()) + } + } + } +} + +const UTXO: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const TAG: &str = "BIP0322-signed-message"; +fn get_spend_tx_id(data: &[u8], script_pub_key: Script) -> Result { + let tag_hash = sha256_hash(&TAG.as_bytes().to_vec()); + let mut to_sign = Vec::new(); + to_sign.extend(tag_hash.clone()); + to_sign.extend(tag_hash); + to_sign.extend(data); + + let hash = sha256_hash(&to_sign); + let mut script_sig = Vec::new(); + script_sig.extend([0x00, 0x20]); + script_sig.extend(hash); + + //Tx ins + let ins = vec![TxIn { + previous_output: OutPoint { + txid: UTXO.parse()?, + vout: 0xFFFFFFFF, + }, + script_sig: Script::from(script_sig), + sequence: Sequence(0), + witness: Witness::new(), + }]; + + //Tx outs + let outs = vec![TxOut { + value: 0, + script_pubkey: script_pub_key, + }]; + + let tx = Transaction { + version: 0, + lock_time: PackedLockTime::ZERO, + input: ins, + output: outs, + }; + + Ok(tx.txid()) +} + +fn create_to_sign_empty(txid: Txid, script_pub_key: Script) -> Result { + //Tx ins + let ins = vec![TxIn { + previous_output: OutPoint { txid, vout: 0 }, + script_sig: Script::new(), + sequence: Sequence(0), + witness: Witness::new(), + }]; + + //Tx outs + let outs = vec![TxOut { + value: 0, + script_pubkey: Script::from(Vec::::from_hex("6a")?), + }]; + + let tx = Transaction { + version: 0, + lock_time: PackedLockTime::ZERO, + input: ins, + output: outs, + }; + + let mut psbt = PartiallySignedTransaction::from_unsigned_tx(tx)?; + psbt.inputs[0].witness_utxo = Some(TxOut { + value: 0, + script_pubkey: script_pub_key, + }); + + Ok(psbt) +} + +fn witness_to_vec(witness: Vec>) -> Vec { + let mut ret: Vec = Vec::new(); + ret.push(witness.len() as u8); + for item in witness { + ret.push(item.len() as u8); + ret.extend(item); + } + ret +} + +#[cfg(test)] +mod tests { + use crate::address::BtcAddress; + use crate::btcapi::BtcMessageInput; + use crate::message::MessageSinger; + use bitcoin::{Address, Network}; + use ikc_common::SignParam; + use ikc_device::device_binding::bind_test; + use std::str::FromStr; + + #[test] + fn test_to_spend_tx_id() { + bind_test(); + + let derivation_path = "m/44'/0'/0'/0/0"; + let pub_key = BtcAddress::get_pub_key(derivation_path).unwrap(); + let network = Network::Bitcoin; + let seg_wit = "VERSION_0"; + let address = BtcAddress::from_public_key(&pub_key, Network::Testnet, seg_wit).unwrap(); + let address = Address::from_str(&address).unwrap(); + let message = "hello world"; + + assert_eq!( + super::get_spend_tx_id(message.as_bytes(), address.script_pubkey()) + .unwrap() + .to_string(), + "24bca2df5140bcf6a6aeafd141ad40b0595aa6998ca0fc733488d7131ca7763f" + ); + } + + #[test] + fn test_bip32_p2sh_p2wpkh() { + bind_test(); + + let singer = MessageSinger { + derivation_path: "m/49'/0'/0'".to_string(), + chain_type: "BITCOIN".to_string(), + network: "MAINNET".to_string(), + seg_wit: "P2WPKH".to_string(), + }; + let input = BtcMessageInput { + message: "hello world".to_string(), + }; + + let output = singer.sign_message(input).unwrap(); + assert_eq!(output.signature, "02473044022000ae3c9439681a4ba05e74d0805210f71c31f92130bcec28934d29beaf5f4f890220327cbf8a189eee4cb35a2599f6fd97b0774bec2e4191d74b3460f746732f8a03012103036695c5f3de2e2792b170f59679d4db88a8516728012eaa42a22ce6f8bf593b"); + } + + #[test] + fn test_bip32_p2pkh() { + bind_test(); + + let singer = MessageSinger { + derivation_path: "m/44'/0'/0'".to_string(), + chain_type: "BITCOIN".to_string(), + network: "MAINNET".to_string(), + seg_wit: "NONE".to_string(), + }; + let input = BtcMessageInput { + message: "hello world".to_string(), + }; + let output = singer.sign_message(input).unwrap(); + assert_eq!(output.signature, "02483045022100dbbdfedfb1902ca12c6cba14d4892a98f77c434daaa4f97fd35e618374c908f602206527ff2b1ce550c16c836c2ce3508bfae543fa6c11759d2f4966cc0d3552c4430121026b5b6a9d041bc5187e0b34f9e496436c7bff261c6c1b5f3c06b433c61394b868"); + } + + #[test] + fn test_bip322_p2wpkh() { + bind_test(); + + let singer = MessageSinger { + derivation_path: "m/44'/0'/0'".to_string(), + chain_type: "BITCOIN".to_string(), + network: "MAINNET".to_string(), + seg_wit: "VERSION_0".to_string(), + }; + let input = BtcMessageInput { + message: "hello world".to_string(), + }; + let output = singer.sign_message(input).unwrap(); + assert_eq!(output.signature, "024830450221009f003820d1db93bf78be08dafdd05b7dde7c31a73c9be36b705a15329bd3d0e502203eb6f1a34466995e4b9c281bf4a093a1f55a21b2ef961438c9ae284efab27dda0121026b5b6a9d041bc5187e0b34f9e496436c7bff261c6c1b5f3c06b433c61394b868"); + } + + #[test] + fn test_bip322_p2tr() { + bind_test(); + + let singer = MessageSinger { + derivation_path: "m/86'/0'/0'".to_string(), + chain_type: "BITCOIN".to_string(), + network: "MAINNET".to_string(), + seg_wit: "VERSION_1".to_string(), + }; + let input = BtcMessageInput { + message: "Sign this message to log in to https://www.subber.xyz // 200323342" + .to_string(), + }; + let output = singer.sign_message(input).unwrap(); + // assert_eq!(output.signature, "0140a868e67a50f6dff3e25f6b015f595d89de54e330a6e1dfb4925269577730803e10a43562b25979a704f1d6c856e623681f292ce0ddf2281f42c033db013b4326"); + } +} diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/network.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/network.rs new file mode 100644 index 00000000..fc1978af --- /dev/null +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/network.rs @@ -0,0 +1,113 @@ +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BtcKinNetwork { + pub coin: &'static str, + pub network: &'static str, + + pub bech32_hrp: &'static str, + pub p2pkh_prefix: u8, + pub p2sh_prefix: u8, + pub private_prefix: u8, + + pub xpub_prefix: [u8; 4], + pub xprv_prefix: [u8; 4], +} +const BTC_KIN_NETWORKS: [BtcKinNetwork; 8] = [ + BtcKinNetwork { + coin: "BITCOIN", + network: "MAINNET", + bech32_hrp: "bc", + p2pkh_prefix: 0x0, + p2sh_prefix: 0x05, + private_prefix: 0x80, + xpub_prefix: [0x04, 0x88, 0xb2, 0x1e], + xprv_prefix: [0x04, 0x88, 0xad, 0xe4], + }, + BtcKinNetwork { + coin: "BITCOIN", + network: "TESTNET", + bech32_hrp: "tb", + p2pkh_prefix: 0x6f, + p2sh_prefix: 0xc4, + private_prefix: 0xef, + xpub_prefix: [0x04, 0x35, 0x87, 0xcf], + xprv_prefix: [0x04, 0x35, 0x83, 0x94], + }, + BtcKinNetwork { + coin: "LITECOIN", + network: "MAINNET", + bech32_hrp: "ltc", + p2pkh_prefix: 0x30, + p2sh_prefix: 0x32, + private_prefix: 0xb0, + xpub_prefix: [0x04, 0x88, 0xb2, 0x1e], + xprv_prefix: [0x04, 0x88, 0xad, 0xe4], + }, + BtcKinNetwork { + coin: "LITECOIN", + network: "TESTNET", + bech32_hrp: "tltc", + p2pkh_prefix: 0x6f, + p2sh_prefix: 0x3a, + private_prefix: 0xef, + xpub_prefix: [0x04, 0x35, 0x87, 0xcf], + xprv_prefix: [0x04, 0x35, 0x83, 0x94], + }, + BtcKinNetwork { + coin: "BITCOINCASH", + network: "MAINNET", + bech32_hrp: "bitcoincash", + p2pkh_prefix: 0x0, + p2sh_prefix: 0x05, + private_prefix: 0x80, + xpub_prefix: [0x04, 0x88, 0xb2, 0x1e], + xprv_prefix: [0x04, 0x88, 0xad, 0xe4], + }, + BtcKinNetwork { + coin: "BITCOINCASH", + network: "TESTNET", + bech32_hrp: "bchtest", + p2pkh_prefix: 0x6f, + p2sh_prefix: 0xc4, + private_prefix: 0xef, + xpub_prefix: [0x04, 0x35, 0x87, 0xcf], + xprv_prefix: [0x04, 0x35, 0x83, 0x94], + }, + BtcKinNetwork { + coin: "DOGECOIN", + network: "TESTNET", + bech32_hrp: "", + p2pkh_prefix: 0x71, + p2sh_prefix: 0xc4, + private_prefix: 0xf1, + xpub_prefix: [0x04, 0x35, 0x87, 0xcf], + xprv_prefix: [0x04, 0x35, 0x83, 0x94], + }, + BtcKinNetwork { + coin: "DOGECOIN", + network: "MAINNET", + bech32_hrp: "", + p2pkh_prefix: 0x1e, + p2sh_prefix: 0x16, + private_prefix: 0x9e, + xpub_prefix: [0x02, 0xfa, 0xca, 0xfd], + xprv_prefix: [0x02, 0xfa, 0xc3, 0x98], + }, +]; + +impl BtcKinNetwork { + pub fn find_by_coin<'a>(coin: &str, network: &str) -> Option<&'a BtcKinNetwork> { + BTC_KIN_NETWORKS + .iter() + .find(|net| net.coin == coin && net.network == network) + } + + pub fn find_by_hrp<'a>(hrp: &str) -> Option<&'a BtcKinNetwork> { + BTC_KIN_NETWORKS.iter().find(|net| net.bech32_hrp == hrp) + } + + pub fn find_by_prefix<'a>(prefix: u8) -> Option<&'a BtcKinNetwork> { + BTC_KIN_NETWORKS + .iter() + .find(|net| net.p2pkh_prefix == prefix || net.p2sh_prefix == prefix) + } +} diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/psbt.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/psbt.rs new file mode 100644 index 00000000..e46e1ba1 --- /dev/null +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/psbt.rs @@ -0,0 +1,1047 @@ +use crate::address::BtcAddress; +use crate::btcapi::{PsbtInput, PsbtOutput}; +use crate::common::{get_address_version, get_xpub_data, select_btc_applet}; +use crate::Result; +use anyhow::anyhow; +use bitcoin::blockdata::script::Builder; +use bitcoin::consensus::{serialize, Decodable, Encodable}; +use bitcoin::psbt::Psbt; +use bitcoin::schnorr::UntweakedPublicKey; +use bitcoin::util::taproot::{TapLeafHash, TapTweakHash}; +use bitcoin::{ + Address, EcdsaSig, EcdsaSighashType, Network, PublicKey, SchnorrSig, SchnorrSighashType, + Script, TxOut, WPubkeyHash, Witness, +}; +use bitcoin_hashes::hex::ToHex; +use bitcoin_hashes::{hash160, Hash}; +use hex::FromHex; +use ikc_common::apdu::{ApduCheck, BtcApdu}; +use ikc_common::coin_info::coin_info_from_param; +use ikc_common::constants; +use ikc_common::constants::TIMEOUT_LONG; +use ikc_common::error::CoinError; +use ikc_common::path::{check_path_validity, get_account_path}; +use ikc_common::utility::{bigint_to_byte_vec, hex_to_bytes, secp256k1_sign, sha256_hash}; +use ikc_device::device_binding::KEY_MANAGER; +use ikc_transport::message::{send_apdu, send_apdu_timeout}; +use secp256k1::{ + ecdsa::Signature, schnorr::Signature as SchnorrSignature, PublicKey as Secp256k1PublicKey, +}; +use std::collections::BTreeMap; +use std::io::Cursor; +use std::str::FromStr; +use std::usize; + +pub struct PsbtSigner<'a> { + psbt: &'a mut Psbt, + derivation_path: String, + auto_finalize: bool, + prevouts: Vec, + network: Network, + preview_output: Vec, + is_sign_message: bool, +} + +impl<'a> PsbtSigner<'a> { + pub fn new( + psbt: &'a mut Psbt, + derivation_path: &str, + auto_finalize: bool, + network: Network, + is_sign_message: bool, + ) -> Result { + let mut psbt_signer = PsbtSigner { + psbt, + derivation_path: derivation_path.to_string(), + prevouts: vec![], + auto_finalize, + network, + preview_output: vec![], + is_sign_message, + }; + psbt_signer.get_preview_output()?; + Ok(psbt_signer) + } + + pub fn sign(&mut self, pub_keys: &Vec) -> Result<()> { + for idx in 0..self.prevouts.len() { + let prevout = &self.prevouts[idx]; + + if prevout.script_pubkey.is_p2pkh() { + self.sign_p2pkh(idx, &pub_keys[idx])?; + + if self.auto_finalize { + self.finalize_p2pkh(idx); + } + } else if prevout.script_pubkey.is_p2sh() { + self.sign_p2sh_nested_p2wpkh(idx, &pub_keys[idx])?; + + if self.auto_finalize { + self.finalize_p2sh_nested_p2wpkh(idx); + } + } else if prevout.script_pubkey.is_v0_p2wpkh() { + self.sign_p2wpkh(idx, &pub_keys[idx])?; + + if self.auto_finalize { + self.finalize_p2wpkh(idx); + } + } else if !self.psbt.inputs.first().unwrap().tap_scripts.is_empty() { + let input = self.psbt.inputs[idx].clone(); + let (_, script_leaf) = input.tap_scripts.first_key_value().unwrap(); + let (script, leaf_version) = script_leaf; + self.sign_p2tr_script( + idx, + &pub_keys[idx], + Some(( + TapLeafHash::from_script(script, leaf_version.clone()).into(), + 0xFFFFFFFF, + )), + )?; + + if self.auto_finalize { + self.finalize_p2tr(idx); + } + } else if prevout.script_pubkey.is_v1_p2tr() { + self.sign_p2tr(idx, &pub_keys[idx])?; + + if self.auto_finalize { + self.finalize_p2tr(idx); + } + } + + if self.auto_finalize { + self.clear_finalized_input(idx); + } + } + + Ok(()) + } + + pub fn prevouts(&mut self) -> Result<()> { + let len = self.psbt.inputs.len(); + let mut utxos = Vec::with_capacity(len); + + for i in 0..len { + let input = &self.psbt.inputs[i]; + let utxo = if let Some(witness_utxo) = &input.witness_utxo { + witness_utxo + } else if let Some(non_witness_utxo) = &input.non_witness_utxo { + let vout = self.psbt.unsigned_tx.input[i].previous_output.vout; + &non_witness_utxo.output[vout as usize] + } else { + return Err(CoinError::InvalidUtxo.into()); + }; + utxos.push(utxo.clone()); + } + self.prevouts = utxos; + Ok(()) + } + + fn get_path(&self, index: usize, is_p2tr: bool) -> Result { + let input = &self.psbt.inputs[index]; + let mut path = if !self.derivation_path.is_empty() { + format!("{}/0/0", self.derivation_path) + } else { + "".to_string() + }; + + if is_p2tr { + let tap_bip32_derivation = input.tap_key_origins.first_key_value(); + + if let Some((_, key_source)) = tap_bip32_derivation { + path = key_source.1 .1.to_string(); + } + } else { + let bip32_derivations = input.bip32_derivation.first_key_value(); + + if let Some((_, key_source)) = bip32_derivations { + path = key_source.1.to_string(); + } + } + Ok(path) + } + + pub fn get_pub_key(&self) -> Result> { + let mut pub_key_vec = vec![]; + for (idx, tx_out) in self.prevouts.iter().enumerate() { + let path = if tx_out.script_pubkey.is_v1_p2tr() { + self.get_path(idx, true)? + } else { + self.get_path(idx, false)? + }; + + let xpub_data = get_xpub_data(&path, false)?; + let derive_pub_key = &xpub_data[..130]; + let public_key = Secp256k1PublicKey::from_str(derive_pub_key)?; + pub_key_vec.push(public_key.to_string()) + } + + Ok(pub_key_vec) + } + + fn sign_p2pkh(&mut self, idx: usize, pub_key: &str) -> Result<()> { + let mut input_data_vec = vec![]; + for (x, prevout) in self.prevouts.iter().enumerate() { + let mut temp_serialize_txin = self + .psbt + .unsigned_tx + .input + .get(x) + .expect("get_input_error") + .clone(); + if x == idx { + temp_serialize_txin.script_sig = prevout.script_pubkey.clone(); + } + input_data_vec.extend_from_slice(serialize(&temp_serialize_txin).as_slice()); + } + input_data_vec.extend(serialize(&self.psbt.unsigned_tx.output)); + let btc_perpare_apdu_list = BtcApdu::btc_single_utxo_sign_prepare(0x50, &input_data_vec); + for apdu in btc_perpare_apdu_list { + ApduCheck::check_response(&send_apdu(apdu)?)?; + } + let path = self.get_path(idx, false)?; + let btc_sign_apdu = + BtcApdu::btc_single_utxo_sign(idx as u8, EcdsaSighashType::All.to_u32() as u8, &path); + + let btc_sign_apdu_return = send_apdu(btc_sign_apdu)?; + ApduCheck::check_response(&btc_sign_apdu_return)?; + let btc_sign_apdu_return = + &btc_sign_apdu_return[..btc_sign_apdu_return.len() - 4].to_string(); + let sign_result_str = btc_sign_apdu_return[2..btc_sign_apdu_return.len() - 2].to_string(); + + let mut signature_obj = Signature::from_compact(&hex::decode(&sign_result_str)?)?; + signature_obj.normalize_s(); + let pub_key = PublicKey::from_str(pub_key)?; + self.psbt.inputs[idx] + .partial_sigs + .insert(pub_key, EcdsaSig::sighash_all(signature_obj)); + + Ok(()) + } + + fn sign_p2sh_nested_p2wpkh(&mut self, idx: usize, pub_key: &str) -> Result<()> { + let temp_serialize_txin = self + .psbt + .unsigned_tx + .input + .get(idx) + .expect("get_input_error") + .clone(); + let prevout = &self.prevouts[idx]; + // let pub_key = &self.get_pub_key(idx, false)?; + let mut data: Vec = vec![]; + //txhash and vout + let txhash_data = serialize(&temp_serialize_txin.previous_output); + data.extend(txhash_data.iter()); + //lock script + let script = Script::new_v0_p2wpkh(&WPubkeyHash::from_hash(hash160::Hash::hash( + &hex_to_bytes(pub_key)?, + ))); + let script = script.p2wpkh_script_code().expect("must be v0_p2wpkh"); + data.extend(serialize(&script).iter()); + //amount + let mut utxo_amount = num_bigint::BigInt::from(prevout.value).to_signed_bytes_le(); + while utxo_amount.len() < 8 { + utxo_amount.push(0x00); + } + data.extend(utxo_amount.iter()); + //set sequence + let sequence = serialize(&temp_serialize_txin.sequence).to_vec(); + data.extend(sequence); + //set length + data.insert(0, data.len() as u8); + //address + let mut address_data: Vec = vec![]; + let sign_path = self.get_path(idx, false)?; + address_data.push(sign_path.as_bytes().len() as u8); + address_data.extend_from_slice(sign_path.as_bytes()); + data.extend(address_data.iter()); + + let sign_apdu = if idx == (self.psbt.unsigned_tx.input.len() - 1) { + BtcApdu::btc_segwit_sign(true, 0x01, data) + } else { + BtcApdu::btc_segwit_sign(false, 0x01, data) + }; + let sign_apdu_return_data = send_apdu(sign_apdu)?; + ApduCheck::check_response(&sign_apdu_return_data)?; + + //build signature obj + let sign_result_vec = + Vec::from_hex(&sign_apdu_return_data[2..sign_apdu_return_data.len() - 6]).unwrap(); + let mut signature_obj = Signature::from_compact(sign_result_vec.as_slice())?; + signature_obj.normalize_s(); + //generator der sign data + let mut sign_result_vec = signature_obj.serialize_der().to_vec(); + //add hash type + sign_result_vec.push(EcdsaSighashType::All.to_u32() as u8); + let pub_key = PublicKey::from_str(pub_key)?; + self.psbt.inputs[idx] + .partial_sigs + .insert(pub_key, EcdsaSig::sighash_all(signature_obj)); + Ok(()) + } + + fn sign_p2wpkh(&mut self, idx: usize, pub_key: &str) -> Result<()> { + let temp_serialize_txin = self + .psbt + .unsigned_tx + .input + .get(idx) + .expect("get_input_error") + .clone(); + let prevout = &self.prevouts[idx]; + let mut data: Vec = vec![]; + //txhash and vout + let txhash_data = serialize(&temp_serialize_txin.previous_output); + data.extend(txhash_data.iter()); + //lock script + let script = prevout + .script_pubkey + .p2wpkh_script_code() + .expect("must be v0_p2wpkh"); + data.extend(serialize(&script).iter()); + //amount + let mut utxo_amount = num_bigint::BigInt::from(prevout.value).to_signed_bytes_le(); + while utxo_amount.len() < 8 { + utxo_amount.push(0x00); + } + data.extend(utxo_amount.iter()); + //set sequence + let sequence = serialize(&temp_serialize_txin.sequence).to_vec(); + data.extend(sequence); + //set length + data.insert(0, data.len() as u8); + //address + let mut address_data: Vec = vec![]; + let sign_path = self.get_path(idx, false)?; + address_data.push(sign_path.as_bytes().len() as u8); + address_data.extend_from_slice(sign_path.as_bytes()); + data.extend(address_data.iter()); + + let sign_apdu = if idx == (self.psbt.unsigned_tx.input.len() - 1) { + BtcApdu::btc_segwit_sign(true, 0x01, data) + } else { + BtcApdu::btc_segwit_sign(false, 0x01, data) + }; + let sign_apdu_return_data = send_apdu(sign_apdu)?; + ApduCheck::check_response(&sign_apdu_return_data)?; + //build signature obj + let sign_result_vec = + Vec::from_hex(&sign_apdu_return_data[2..sign_apdu_return_data.len() - 6]).unwrap(); + let mut signature_obj = Signature::from_compact(sign_result_vec.as_slice())?; + signature_obj.normalize_s(); + let pub_key = PublicKey::from_str(pub_key)?; + self.psbt.inputs[idx] + .partial_sigs + .insert(pub_key, EcdsaSig::sighash_all(signature_obj)); + Ok(()) + } + + fn sign_p2tr(&mut self, idx: usize, pub_key: &str) -> Result<()> { + let mut data: Vec = vec![]; + // epoch (1). + data.push(0x00u8); + // hash_type (1). + data.push(SchnorrSighashType::Default as u8); + //nVersion (4): + //nLockTime (4) + // data.extend(serialize(&PackedLockTime::ZERO)); + data.extend(serialize(&self.psbt.unsigned_tx.lock_time)); + //prevouts_hash + amounts_hash + script_pubkeys_hash + sequences_hash + sha_outputs (32) + //spend_type (1) + data.push(0x00u8); + //input_index (4) + data.extend(serialize(&(idx as u32))); + + let mut path_data: Vec = vec![]; + let sign_path = self.get_path(idx, true)?; + path_data.push(sign_path.as_bytes().len() as u8); + path_data.extend_from_slice(sign_path.as_bytes()); + data.extend(path_data.iter()); + + let mut tweaked_pub_key_data: Vec = vec![]; + let untweaked_public_key = UntweakedPublicKey::from_str(&pub_key[2..66])?; + let tweaked_pub_key = TapTweakHash::from_key_and_tweak(untweaked_public_key, None).to_vec(); + tweaked_pub_key_data.push(tweaked_pub_key.len() as u8); + tweaked_pub_key_data.extend_from_slice(&tweaked_pub_key); + data.extend(tweaked_pub_key_data.iter()); + + let sign_apdu = if idx == (self.psbt.unsigned_tx.input.len() - 1) { + BtcApdu::btc_taproot_sign(true, data) + } else { + BtcApdu::btc_taproot_sign(false, data) + }; + let sign_result = send_apdu(sign_apdu)?; + ApduCheck::check_response(&sign_result)?; + + let sign_bytes = hex_to_bytes(&sign_result[2..(sign_result.len() - 4)])?; + let sig = SchnorrSignature::from_slice(&sign_bytes)?; + self.psbt.inputs[idx].tap_key_sig = Some(SchnorrSig { + hash_ty: SchnorrSighashType::Default, + sig, + }); + + Ok(()) + } + + fn sign_p2tr_script( + &mut self, + idx: usize, + pub_key: &str, + leaf_hash_code_separator: Option<(TapLeafHash, u32)>, + ) -> Result<()> { + let mut data: Vec = vec![]; + // epoch (1). + data.push(0x00u8); + // hash_type (1). + data.push(SchnorrSighashType::Default as u8); + //nVersion (4): + //nLockTime (4) + // data.extend(serialize(&PackedLockTime::ZERO)); + data.extend(serialize(&self.psbt.unsigned_tx.lock_time)); + //prevouts_hash + amounts_hash + script_pubkeys_hash + sequences_hash + sha_outputs (32) + //spend_type (1) + let mut spend_type = 0u8; + if leaf_hash_code_separator.is_some() { + spend_type |= 2u8; + } + data.push(spend_type); + //input_index (4) + data.extend(serialize(&(idx as u32))); + //leaf hash code separator + if let Some((hash, code_separator_pos)) = leaf_hash_code_separator { + let mut temp_data = hash.into_inner().to_vec(); + temp_data.push(0x00u8); //key_version_0 + let code_separator_pos = code_separator_pos.to_be_bytes(); + temp_data.extend(code_separator_pos); + data.push(temp_data.len() as u8); + data.extend(temp_data); + } + let mut path_data: Vec = vec![]; + let sign_path = self.get_path(idx, true)?; + path_data.push(sign_path.as_bytes().len() as u8); + path_data.extend_from_slice(sign_path.as_bytes()); + data.extend(path_data.iter()); + let mut tweaked_pub_key_data: Vec = vec![]; + let untweaked_public_key = UntweakedPublicKey::from_str(&pub_key[2..66])?; + let tweaked_pub_key = TapTweakHash::from_key_and_tweak(untweaked_public_key, None).to_vec(); + tweaked_pub_key_data.push(tweaked_pub_key.len() as u8); + tweaked_pub_key_data.extend_from_slice(&tweaked_pub_key); + data.extend(tweaked_pub_key_data.iter()); + + let sign_apdu = if idx == (self.psbt.unsigned_tx.input.len() - 1) { + BtcApdu::btc_taproot_script_sign(true, data) + } else { + BtcApdu::btc_taproot_script_sign(false, data) + }; + + let sign_result = send_apdu(sign_apdu)?; + ApduCheck::check_response(&sign_result)?; + + let sign_bytes = hex_to_bytes(&sign_result[2..(sign_result.len() - 4)])?; + let sig = SchnorrSignature::from_slice(&sign_bytes)?; + self.psbt.inputs[idx].tap_key_sig = Some(SchnorrSig { + hash_ty: SchnorrSighashType::Default, + sig, + }); + + Ok(()) + } + + pub fn calc_tx_hash(&self) -> Result<()> { + let mut txhash_vout_vec = vec![]; + let mut sequence_vec = vec![]; + let mut amount_vec = vec![]; + let mut script_pubkeys_vec = vec![]; + for (idx, tx_in) in self.psbt.unsigned_tx.input.iter().enumerate() { + let prevout = &self.prevouts[idx]; + txhash_vout_vec.extend(serialize(&tx_in.previous_output)); + sequence_vec.extend(serialize(&tx_in.sequence)); + amount_vec.extend(serialize(&prevout.value)); + script_pubkeys_vec.extend(serialize(&prevout.script_pubkey)); + } + let mut calc_hash_apdu = vec![]; + calc_hash_apdu.extend(BtcApdu::btc_prepare(0x31, 0x40, &txhash_vout_vec)); + calc_hash_apdu.extend(BtcApdu::btc_prepare(0x31, 0x80, &sequence_vec)); + calc_hash_apdu.extend(BtcApdu::btc_prepare(0x31, 0x20, &amount_vec)); + calc_hash_apdu.extend(BtcApdu::btc_prepare(0x31, 0x21, &script_pubkeys_vec)); + for apdu in calc_hash_apdu { + ApduCheck::check_response(&send_apdu(apdu)?)?; + } + Ok(()) + } + + pub fn tx_preview(&self, network: Network) -> Result<()> { + let (total_amount, fee, _outputs) = self.get_preview_info()?; + let mut preview_data = vec![]; + preview_data.extend(&serialize(&self.psbt.unsigned_tx.version)); //version + let input_number = self.psbt.unsigned_tx.input.len(); + preview_data.push(input_number as u8); //input number + preview_data.extend(&serialize(&self.psbt.unsigned_tx.lock_time)); //lock time + let mut sign_hash_type = Vec::new(); + let len = EcdsaSighashType::All + .to_u32() + .consensus_encode(&mut sign_hash_type) + .unwrap(); + debug_assert_eq!(len, sign_hash_type.len()); + preview_data.extend(&sign_hash_type); //hash type + preview_data.extend(bigint_to_byte_vec(total_amount)); //total payment amount + preview_data.extend(bigint_to_byte_vec(fee)); //fee + let mut output_serialize = vec![]; + for tx_out in self.psbt.unsigned_tx.output.iter() { + output_serialize.extend(serialize(tx_out)); + } + let hash = &sha256_hash(&output_serialize); + preview_data.extend_from_slice(hash); //output hash + let display_number = self.preview_output.len() as u16; + preview_data.extend(display_number.to_be_bytes()); + + //set 01 tag and length + preview_data.insert(0, preview_data.len() as u8); + preview_data.insert(0, 0x01); + + //use local private key sign data + let key_manager_obj = KEY_MANAGER.lock(); + let mut output_pareper_data = secp256k1_sign(&key_manager_obj.pri_key, &preview_data)?; + output_pareper_data.insert(0, output_pareper_data.len() as u8); + output_pareper_data.insert(0, 0x00); + output_pareper_data.extend(preview_data.iter()); + let btc_prepare_apdu_vec = BtcApdu::btc_prepare(0x4B, 0x00, &output_pareper_data); + for temp_str in btc_prepare_apdu_vec { + ApduCheck::check_response(&send_apdu(temp_str)?)?; + } + + let mut page_number = 0; + loop { + let mut outputs_data = if self.is_sign_message { + vec![0xFF, 0xFF] + } else { + self.serizalize_page_data(page_number, network)? + }; + //set 01 tag and length + outputs_data.insert(0, outputs_data.len() as u8); + outputs_data.insert(0, 0x01); + //use local private key sign data + let mut output_pareper_data = secp256k1_sign(&key_manager_obj.pri_key, &outputs_data)?; + output_pareper_data.insert(0, output_pareper_data.len() as u8); + output_pareper_data.insert(0, 0x00); + output_pareper_data.extend(outputs_data.iter()); + let sign_confirm = if self.is_sign_message { + BtcApdu::btc_psbt_preview(&output_pareper_data, 0x80) + } else { + BtcApdu::btc_psbt_preview(&output_pareper_data, 0x00) + }; + let response = &send_apdu_timeout(sign_confirm, TIMEOUT_LONG)?; + ApduCheck::check_response(response)?; + if response.len() > 4 { + let page_index = &response[..response.len() - 4]; + page_number = u16::from_str_radix(page_index, 16)? as usize; + } else { + break; + } + } + Ok(()) + } + + fn finalize_p2pkh(&mut self, index: usize) { + let input = &mut self.psbt.inputs[index]; + + if !input.partial_sigs.is_empty() { + let sig = input.partial_sigs.first_key_value().unwrap(); + + input.final_script_sig = Some( + Builder::new() + .push_slice(&sig.1.to_vec()) + .push_slice(&sig.0.to_bytes()) + .into_script(), + ); + } + } + + fn finalize_p2sh_nested_p2wpkh(&mut self, index: usize) { + let input = &mut self.psbt.inputs[index]; + + if !input.partial_sigs.is_empty() { + let sig = input.partial_sigs.first_key_value().unwrap(); + + let script = + Script::new_v0_p2wpkh(&WPubkeyHash::from_hash(Self::hash160(&sig.0.to_bytes()))); + + input.final_script_sig = Some(script); + + let mut witness = Witness::new(); + witness.push(sig.1.to_vec()); + witness.push(sig.0.to_bytes()); + + input.final_script_witness = Some(witness); + } + } + + fn finalize_p2wpkh(&mut self, index: usize) { + let input = &mut self.psbt.inputs[index]; + + if !input.partial_sigs.is_empty() { + let sig = input.partial_sigs.first_key_value().unwrap(); + let mut witness = Witness::new(); + + witness.push(sig.1.to_vec()); + witness.push(sig.0.to_bytes()); + + input.final_script_witness = Some(witness) + } + } + + fn finalize_p2tr(&mut self, index: usize) { + let input = &mut self.psbt.inputs[index]; + + if input.tap_key_sig.is_some() { + let mut witness = Witness::new(); + witness.push(input.tap_key_sig.unwrap().to_vec()); + + if !input.tap_scripts.is_empty() { + let (control_block, script_leaf) = input.tap_scripts.first_key_value().unwrap(); + + let (script, _) = script_leaf; + witness.push(script.as_bytes().to_vec()); + witness.push(control_block.serialize()) + } + + input.final_script_witness = Some(witness); + } + } + + fn clear_finalized_input(&mut self, index: usize) { + let input = &mut self.psbt.inputs[index]; + input.tap_key_sig = None; + input.tap_scripts = BTreeMap::new(); + input.tap_internal_key = None; + input.tap_merkle_root = None; + input.tap_script_sigs = BTreeMap::new(); + input.partial_sigs = BTreeMap::new(); + input.sighash_type = None; + input.redeem_script = None; + input.witness_script = None; + input.bip32_derivation = BTreeMap::new(); + input.unknown = BTreeMap::new(); + } + + fn hash160(input: &[u8]) -> hash160::Hash { + Hash::hash(input) + } + + fn get_preview_output(&mut self) -> Result<()> { + let mut preview_output: Vec = vec![]; + + for tx_out in self.psbt.unsigned_tx.output.iter() { + //remove empty and op_return TxOut + if tx_out.script_pubkey.is_empty() || tx_out.script_pubkey.is_op_return() { + continue; + } + //cale change index script + let tx_out_script = &tx_out.script_pubkey; + let address = if tx_out_script.is_p2pkh() { + let change_path = + Self::get_change_index(self.network, constants::BTC_SEG_WIT_TYPE_LEGACY)?; + let pub_key = BtcAddress::get_pub_key(&change_path)?; + BtcAddress::from_public_key( + &pub_key, + self.network, + constants::BTC_SEG_WIT_TYPE_LEGACY, + )? + } else if tx_out_script.is_v0_p2wpkh() { + let change_path = + Self::get_change_index(self.network, constants::BTC_SEG_WIT_TYPE_VERSION_0)?; + let pub_key = BtcAddress::get_pub_key(&change_path)?; + BtcAddress::from_public_key( + &pub_key, + self.network, + constants::BTC_SEG_WIT_TYPE_VERSION_0, + )? + } else if tx_out_script.is_p2sh() { + let change_path = + Self::get_change_index(self.network, constants::BTC_SEG_WIT_TYPE_P2WPKH)?; + let pub_key = BtcAddress::get_pub_key(&change_path)?; + BtcAddress::from_public_key( + &pub_key, + self.network, + constants::BTC_SEG_WIT_TYPE_P2WPKH, + )? + } else if tx_out_script.is_v1_p2tr() { + let change_path = + Self::get_change_index(self.network, constants::BTC_SEG_WIT_TYPE_VERSION_1)?; + let pub_key = BtcAddress::get_pub_key(&change_path)?; + BtcAddress::from_public_key( + &pub_key, + self.network, + constants::BTC_SEG_WIT_TYPE_VERSION_1, + )? + } else { + continue; + }; + //remove change TxOut + let script_hex = Address::from_str(&address)?.script_pubkey().to_hex(); + if script_hex.eq(&tx_out.script_pubkey.to_hex()) { + continue; + } + preview_output.push(tx_out.clone()); + } + self.preview_output = preview_output; + Ok(()) + } + + pub fn get_preview_info(&self) -> Result<(u64, u64, Vec)> { + let outputs = &self.preview_output; + let payment_total_amount = outputs.iter().map(|tx_out| tx_out.value).sum(); + let input_total_amount: u64 = self.prevouts.iter().map(|tx_out| tx_out.value).sum(); + let output_total_amount: u64 = self + .psbt + .unsigned_tx + .output + .iter() + .map(|tx_out| tx_out.value) + .sum(); + let fee = input_total_amount - output_total_amount; + Ok((payment_total_amount, fee, outputs.clone())) + } + + fn get_page_indices(total_number: usize, page_number: usize) -> Result<(usize, usize)> { + let total_pages = (total_number + constants::BTC_PSBT_TRX_PER_PAGE_NUMBER - 1) + / constants::BTC_PSBT_TRX_PER_PAGE_NUMBER; + if page_number >= total_pages { + return Err(anyhow!("page_number_out_of_range")); + } + let start_index = page_number * constants::BTC_PSBT_TRX_PER_PAGE_NUMBER; + let end_index = usize::min( + total_number, + start_index + constants::BTC_PSBT_TRX_PER_PAGE_NUMBER, + ) - 1; + + if start_index >= total_number { + Ok((total_number, total_number - 1)) + } else if end_index >= total_number { + Ok((start_index, total_number - 1)) + } else { + Ok((start_index, end_index)) + } + } + + fn serizalize_page_data(&self, page_number: usize, network: Network) -> Result> { + let preview_output = &self.preview_output; + let (start_index, end_index) = Self::get_page_indices(preview_output.len(), page_number)?; + let mut data = vec![]; + data.extend((start_index as u16).to_be_bytes()); + data.extend((end_index as u16).to_be_bytes()); + for (index, output) in preview_output.iter().enumerate() { + if start_index <= index && end_index >= index { + let i_u16 = index as u16; + data.extend(i_u16.to_be_bytes()); + data.extend(serialize(&output.value)); + let address = Address::from_script(&output.script_pubkey, network)?; + let address_version = get_address_version(network, &address.to_string())?; + let script_bytes = serialize(&output.script_pubkey); + data.push((1 + script_bytes.len()) as u8); + data.push(address_version); + data.extend(script_bytes); + } + } + + Ok(data) + } + + fn get_change_index(network: Network, segwit: &str) -> Result { + let network = match network { + Network::Bitcoin => "MAINNET", + _ => "TESTNET", + }; + let coin_info = coin_info_from_param("BITCOIN", network, segwit, "secp256k1")?; + let change_path = get_account_path(&coin_info.derivation_path)?; + Ok(change_path) + } +} + +pub fn sign_psbt( + derivation_path: &str, + psbt_input: PsbtInput, + network: Network, +) -> Result { + check_path_validity(derivation_path)?; + + select_btc_applet()?; + + let mut reader = Cursor::new(Vec::::from_hex(psbt_input.psbt)?); + let mut psbt = Psbt::consensus_decode(&mut reader)?; + let mut signer = PsbtSigner::new( + &mut psbt, + derivation_path, + psbt_input.auto_finalize, + network, + false, + )?; + + signer.prevouts()?; + + let pub_keys = signer.get_pub_key()?; + + signer.calc_tx_hash()?; + + signer.get_preview_info()?; + + signer.tx_preview(network)?; + + signer.sign(&pub_keys)?; + + let mut vec = Vec::::new(); + let mut writer = Cursor::new(&mut vec); + psbt.consensus_encode(&mut writer)?; + + return Ok(PsbtOutput { + psbt: hex::encode(vec), + }); +} + +#[cfg(test)] +mod test { + use crate::btcapi::PsbtInput; + use crate::common::select_btc_applet; + use crate::psbt::PsbtSigner; + use bitcoin::consensus::Decodable; + use bitcoin::psbt::serialize::{Deserialize, Serialize}; + use bitcoin::psbt::Psbt; + use bitcoin::schnorr::TapTweak; + use bitcoin::util::bip32::DerivationPath; + use bitcoin::{schnorr, Address, Network, Transaction, TxOut}; + use bitcoin_hashes::hex::ToHex; + use hex::FromHex; + use ikc_device::device_binding::bind_test; + use secp256k1::schnorr::Signature; + use secp256k1::{Message, XOnlyPublicKey}; + use std::io::Cursor; + use std::str::FromStr; + + #[test] + fn test_sign_psbt_no_script() { + bind_test(); + + let psbt_input = PsbtInput { + psbt: "70736274ff0100db0200000001fa4c8d58b9b6c56ed0b03f78115246c99eb70f99b837d7b4162911d1016cda340200000000fdffffff0350c30000000000002251202114eda66db694d87ff15ddd5d3c4e77306b6e6dd5720cbd90cd96e81016c2b30000000000000000496a47626274340066f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229ec20acf33c17e5a6c92cced9f1d530cccab7aa3e53400456202f02fac95e9c481fa00d47b1700000000002251208f4ca6a7384f50a1fe00cba593d5a834b480c65692a76ae6202e1ce46cb1c233d80f03000001012be3bf1d00000000002251208f4ca6a7384f50a1fe00cba593d5a834b480c65692a76ae6202e1ce46cb1c23301172066f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e00000000".to_string(), + auto_finalize: true, + }; + + let psbt_output = super::sign_psbt("m/86'/1'/0'", psbt_input, Network::Bitcoin).unwrap(); + let mut reader = Cursor::new(Vec::::from_hex(&psbt_output.psbt).unwrap()); + let psbt = Psbt::consensus_decode(&mut reader).unwrap(); + let tx = psbt.extract_tx(); + let sig = schnorr::SchnorrSig::from_slice(&tx.input[0].witness.to_vec()[0]).unwrap(); + + let data = + Vec::::from_hex("3a66cf6ec1a87b10b86fa358baf64484bba8c61c9828e5cbe2eb8a3d4bbf190c") + .unwrap(); + let msg = Message::from_slice(&data).unwrap(); + let x_pub_key = XOnlyPublicKey::from_slice( + Vec::::from_hex("66f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e") + .unwrap() + .as_slice(), + ) + .unwrap(); + let SECP256K1_ENGINE: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); + let tweak_pub_key = x_pub_key.tap_tweak(&SECP256K1_ENGINE, None); + + // assert!(sig.sig.verify(&msg, &tweak_pub_key.0.to_inner()).is_ok()); + } + + #[test] + fn test_sign_psbt_script() { + bind_test(); + + let psbt_input = PsbtInput { + psbt: "70736274ff01005e02000000012bd2f6479f3eeaffe95c03b5fdd76a873d346459114dec99c59192a0cb6409e90000000000ffffffff01409c000000000000225120677cc88dc36a75707b370e27efff3e454d446ad55004dac1685c1725ee1a89ea000000000001012b50c3000000000000225120a9a3350206de400f09a73379ec1bcfa161fc11ac095e5f3d7354126f0ec8e87f6215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0d2956573f010fa1a3c135279c5eb465ec2250205dcdfe2122637677f639b1021356c963cd9c458508d6afb09f3fa2f9b48faec88e75698339a4bbb11d3fc9b0efd570120aff94eb65a2fe773a57c5bd54e62d8436a5467573565214028422b41bd43e29bad200aee0509b16db71c999238a4827db945526859b13c95487ab46725357c9a9f25ac20113c3a32a9d320b72190a04a020a0db3976ef36972673258e9a38a364f3dc3b0ba2017921cf156ccb4e73d428f996ed11b245313e37e27c978ac4d2cc21eca4672e4ba203bb93dfc8b61887d771f3630e9a63e97cbafcfcc78556a474df83a31a0ef899cba2040afaf47c4ffa56de86410d8e47baa2bb6f04b604f4ea24323737ddc3fe092dfba2079a71ffd71c503ef2e2f91bccfc8fcda7946f4653cef0d9f3dde20795ef3b9f0ba20d21faf78c6751a0d38e6bd8028b907ff07e9a869a43fc837d6b3f8dff6119a36ba20f5199efae3f28bb82476163a7e458c7ad445d9bffb0682d10d3bdb2cb41f8e8eba20fa9d882d45f4060bdb8042183828cd87544f1ea997380e586cab77d5fd698737ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000".to_string(), + auto_finalize: true, + }; + + let psbt_output = super::sign_psbt("m/86'/1'/0'", psbt_input, Network::Bitcoin).unwrap(); + let mut reader = Cursor::new(Vec::::from_hex(&psbt_output.psbt).unwrap()); + let psbt = Psbt::consensus_decode(&mut reader).unwrap(); + let tx = psbt.extract_tx(); + let witness = tx.input[0].witness.to_vec(); + let sig = schnorr::SchnorrSig::from_slice(&witness[0]).unwrap(); + + let data = + Vec::::from_hex("56b6c5fd09753fbbbeb8f530308e4f7d2f404e02da767f033e926d27fcc2f37e") + .unwrap(); + let msg = Message::from_slice(&data).unwrap(); + let x_pub_key = XOnlyPublicKey::from_slice( + Vec::::from_hex("66f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e") + .unwrap() + .as_slice(), + ) + .unwrap(); + + let script = hex::encode(&witness[1]); + let control_block = hex::encode(&witness[2]); + assert_eq!(script, "20aff94eb65a2fe773a57c5bd54e62d8436a5467573565214028422b41bd43e29bad200aee0509b16db71c999238a4827db945526859b13c95487ab46725357c9a9f25ac20113c3a32a9d320b72190a04a020a0db3976ef36972673258e9a38a364f3dc3b0ba2017921cf156ccb4e73d428f996ed11b245313e37e27c978ac4d2cc21eca4672e4ba203bb93dfc8b61887d771f3630e9a63e97cbafcfcc78556a474df83a31a0ef899cba2040afaf47c4ffa56de86410d8e47baa2bb6f04b604f4ea24323737ddc3fe092dfba2079a71ffd71c503ef2e2f91bccfc8fcda7946f4653cef0d9f3dde20795ef3b9f0ba20d21faf78c6751a0d38e6bd8028b907ff07e9a869a43fc837d6b3f8dff6119a36ba20f5199efae3f28bb82476163a7e458c7ad445d9bffb0682d10d3bdb2cb41f8e8eba20fa9d882d45f4060bdb8042183828cd87544f1ea997380e586cab77d5fd698737ba569c"); + assert_eq!(control_block, "c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0d2956573f010fa1a3c135279c5eb465ec2250205dcdfe2122637677f639b1021356c963cd9c458508d6afb09f3fa2f9b48faec88e75698339a4bbb11d3fc9b0e"); + + // assert!(sig.sig.verify(&msg, &x_pub_key).is_ok()); + } + + #[test] + fn test_sign_psbt_multipayment() { + bind_test(); + + let raw_tx = "02000000054adc61444e5a4dd7021e52dc6f5adadd9a3286d346f5d9f023ebcde2af80a0ae0000000000ffffffff4adc61444e5a4dd7021e52dc6f5adadd9a3286d346f5d9f023ebcde2af80a0ae0100000000ffffffff12cc8049bf85b5e18cb2be8aa7aefc3afb8df4ec5c1f766750014cc95ca2dc130000000000ffffffff729e6570928cc65200f1d53def65a7934d2e9b543059d90598ed1d166af422010100000000ffffffffa126724475cd2f3252352b3543c8455c7999a8283883bd7a712a7d66609d92d80100000000ffffffff02409c00000000000022512036079c540758a51a86eeaf9e17668d4d8543d8b1b7e56fe2da0982c390c5655ef8fa0700000000002251209303a116174dd21ea473766659568ac24eb6b828c3ee998982d2ba070ea0615500000000"; + let mut tx = Transaction::deserialize(&Vec::from_hex(&raw_tx).unwrap()).unwrap(); + + let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); + let fake_pub_key = secp256k1::PublicKey::from_slice( + &Vec::::from_hex( + "0266f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e", + ) + .unwrap(), + ) + .unwrap(); + let fake_xonly_pub_key = XOnlyPublicKey::from_slice( + Vec::::from_hex("66f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e") + .unwrap() + .as_slice(), + ) + .unwrap(); + + psbt.inputs[0].tap_key_origins.insert( + fake_xonly_pub_key, + ( + Default::default(), + ( + Default::default(), + DerivationPath::from_str("m/86'/1'/0'/0/0").unwrap(), + ), + ), + ); + psbt.inputs[0].witness_utxo = Some(TxOut { + value: 20000, + script_pubkey: Address::from_str( + "tb1p3ax2dfecfag2rlsqewje84dgxj6gp3jkj2nk4e3q9cwwgm93cgesa0zwj4", + ) + .unwrap() + .script_pubkey(), + }); + + psbt.inputs[1].tap_key_origins.insert( + fake_xonly_pub_key, + ( + Default::default(), + ( + Default::default(), + DerivationPath::from_str("m/86'/1'/0'/1/53").unwrap(), + ), + ), + ); + psbt.inputs[1].witness_utxo = Some(TxOut { + value: 283000, + script_pubkey: Address::from_str( + "tb1pjvp6z9shfhfpafrnwen9j452cf8tdwpgc0hfnzvz62aqwr4qv92sg7qj9r", + ) + .unwrap() + .script_pubkey(), + }); + + psbt.inputs[2].bip32_derivation.insert( + fake_pub_key, + ( + Default::default(), + DerivationPath::from_str("m/84'/1'/0'/0/0").unwrap(), + ), + ); + psbt.inputs[2].witness_utxo = Some(TxOut { + value: 100000, + script_pubkey: Address::from_str("tb1qrfaf3g4elgykshfgahktyaqj2r593qkrae5v95") + .unwrap() + .script_pubkey(), + }); + + psbt.inputs[3].bip32_derivation.insert( + fake_pub_key, + ( + Default::default(), + DerivationPath::from_str("m/49'/1'/0'/0/0").unwrap(), + ), + ); + psbt.inputs[3].witness_utxo = Some(TxOut { + value: 100000, + script_pubkey: Address::from_str("2MwN441dq8qudMvtM5eLVwC3u4zfKuGSQAB") + .unwrap() + .script_pubkey(), + }); + + psbt.inputs[4].bip32_derivation.insert( + fake_pub_key, + ( + Default::default(), + DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(), + ), + ); + psbt.inputs[4].witness_utxo = Some(TxOut { + value: 100000, + script_pubkey: Address::from_str("mkeNU5nVnozJiaACDELLCsVUc8Wxoh1rQN") + .unwrap() + .script_pubkey(), + }); + + select_btc_applet().unwrap(); + + let mut signer = PsbtSigner::new(&mut psbt, "", true, Network::Testnet, false).unwrap(); + + signer.prevouts().unwrap(); + + let pub_keys = signer.get_pub_key().unwrap(); + + signer.calc_tx_hash().unwrap(); + + signer.get_preview_info().unwrap(); + + signer.tx_preview(Network::Bitcoin).unwrap(); + + signer.sign(&pub_keys).unwrap(); + + let tx = psbt.extract_tx(); + + let msg = Message::from_slice( + &Vec::from_hex("f01ba76b329132e48188ad10d00791647ee6d2f7fee5ef397f3481993c898de3") + .unwrap(), + ) + .unwrap(); + let sig = Signature::from_slice(&tx.input[0].witness.to_vec()[0]).unwrap(); + let pub_key = XOnlyPublicKey::from_slice( + &Vec::from_hex("8f4ca6a7384f50a1fe00cba593d5a834b480c65692a76ae6202e1ce46cb1c233") + .unwrap(), + ) + .unwrap(); + // assert!(sig.verify(&msg, &pub_key).is_ok()); + + let msg = Message::from_slice( + &Vec::from_hex("d0691b5ac1b338b9341790ea69417cb454cf346a718342fb4a846dbb8ae142e8") + .unwrap(), + ) + .unwrap(); + let sig = Signature::from_slice(&tx.input[1].witness.to_vec()[0]).unwrap(); + let pub_key = XOnlyPublicKey::from_slice( + &Vec::from_hex("9303a116174dd21ea473766659568ac24eb6b828c3ee998982d2ba070ea06155") + .unwrap(), + ) + .unwrap(); + // assert!(sig.verify(&msg, &pub_key).is_ok()); + + assert_eq!(tx.input[2].witness.to_vec()[0].to_hex(), "3044022022c2feaa4a225496fc6789c969fb776da7378f44c588ad812a7e1227ebe69b6302204fc7bf5107c6d02021fe4833629bc7ab71cefe354026ebd0d9c0da7d4f335f9401"); + assert_eq!( + tx.input[2].witness.to_vec()[1].to_hex(), + "02e24f625a31c9a8bae42239f2bf945a306c01a450a03fd123316db0e837a660c0" + ); + + assert_eq!(tx.input[3].witness.to_vec()[0].to_hex(), "3045022100dec4d3fd189b532ef04f41f68319ff7dc6a7f2351a0a8f98cb7f1ec1f6d71c7a02205e507162669b642fdb480a6c496abbae5f798bce4fd42cc390aa58e3847a1b9101"); + assert_eq!( + tx.input[3].witness.to_vec()[1].to_hex(), + "031aee5e20399d68cf0035d1a21564868f22bc448ab205292b4279136b15ecaebc" + ); + + assert_eq!(tx.input[4].script_sig.to_hex(), "483045022100ca32abc7b180c84cf76907e4e1e0c3f4c0d6e64de23b0708647ac6fee1c04c5b02206e7412a712424eb9406f18e00a42e0dffbfb5901932d1ef97843d9273865550e0121033d710ab45bb54ac99618ad23b3c1da661631aa25f23bfe9d22b41876f1d46e4e"); + } +} diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/transaction.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/transaction.rs index 6721e1ba..9fdcca3e 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/src/transaction.rs +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/transaction.rs @@ -1,52 +1,60 @@ -use crate::address::BtcAddress; -use crate::common::{address_verify, get_address_version, TxSignResult}; +use crate::btc_kin_address::{AddressTrait, BtcKinAddress, ImkeyPublicKey}; +use crate::common::{get_address_version, get_utxo_pub_key, TxSignResult}; +use crate::network::BtcKinNetwork; use crate::Result; use bitcoin::blockdata::{opcodes, script::Builder}; use bitcoin::consensus::{serialize, Encodable}; use bitcoin::hashes::hex::FromHex; -use bitcoin::psbt::serialize::Serialize; -use bitcoin::schnorr::{TapTweak, UntweakedPublicKey}; +use bitcoin::schnorr::UntweakedPublicKey; use bitcoin::util::taproot::TapTweakHash; use bitcoin::{ - Address, EcdsaSighashType, Network, OutPoint, PackedLockTime, SchnorrSighashType, Script, - Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, + Address, EcdsaSighashType, OutPoint, PackedLockTime, SchnorrSighashType, Script, Sequence, + Transaction, TxIn, TxOut, WPubkeyHash, Witness, }; use bitcoin_hashes::hash160; use bitcoin_hashes::hex::ToHex; use bitcoin_hashes::Hash; use ikc_common::apdu::{ApduCheck, BtcApdu}; -use ikc_common::constants::{MAX_OPRETURN_SIZE, MAX_UTXO_NUMBER, MIN_NONDUST_OUTPUT, TIMEOUT_LONG}; -use ikc_common::error::CoinError; +use ikc_common::constants::{ + BTC_SEG_WIT_TYPE_P2WPKH, EACH_ROUND_NUMBER, MAX_OPRETURN_SIZE, MAX_UTXO_NUMBER, + MIN_NONDUST_OUTPUT, TIMEOUT_LONG, +}; +use ikc_common::error::{CoinError, CommonError}; use ikc_common::path::{check_path_validity, get_account_path}; -use ikc_common::utility::{bigint_to_byte_vec, hex_to_bytes, secp256k1_sign}; +use ikc_common::utility::{bigint_to_byte_vec, hex_to_bytes, network_convert, secp256k1_sign}; use ikc_device::device_binding::KEY_MANAGER; +use ikc_device::device_manager::get_btc_apple_version; use ikc_transport::message::{send_apdu, send_apdu_timeout}; use secp256k1::ecdsa::Signature; -use std::borrow::Borrow; +use secp256k1::PublicKey; use std::str::FromStr; +//The address version set supported by bitcoin applet versions lower than 1.6.00 +const VALID_ADDRESS_VERSIONS: [u8; 7] = [0, 111, 5, 196, 113, 30, 22]; + #[derive(Clone)] pub struct Utxo { pub txhash: String, pub vout: u32, pub amount: u64, - pub address: Address, + pub address: String, pub script_pubkey: String, pub derive_path: String, pub sequence: i64, } pub struct BtcTransaction { - pub to: Address, + pub to: String, pub amount: u64, pub unspents: Vec, pub fee: u64, + pub chain_type: String, } impl BtcTransaction { - pub fn sign_Transaction( + pub fn sign_transaction( &self, - network: Network, + network: &str, path: &str, change_idx: Option, extra_data: Option<&str>, @@ -65,8 +73,11 @@ impl BtcTransaction { return Err(CoinError::ImkeyInsufficientFunds.into()); } + //get current btc applet version + let btc_version = get_btc_apple_version()?; + //utxo address verify - let utxo_pub_key_vec = address_verify(&self.unspents, network)?; + let utxo_pub_key_vec = get_utxo_pub_key(&self.unspents)?; let output = self.tx_output(change_idx, &path, network, seg_wit, extra_data)?; @@ -77,29 +88,60 @@ impl BtcTransaction { output, }; - self.calc_tx_hash(&mut tx_to_sign)?; - - self.tx_preview(&tx_to_sign, network)?; - - let input_with_sigs: Vec = vec![]; - for (idx, utxo) in self.unspents.iter().enumerate() { - let script = Script::from_str(&utxo.script_pubkey)?; - if script.is_p2pkh() { - self.sign_p2pkh_input(idx, &utxo_pub_key_vec[idx], &mut tx_to_sign)?; - } else if script.is_p2sh() { - self.sign_p2sh_nested_p2wpkh_input(idx, &utxo_pub_key_vec[idx], &mut tx_to_sign)?; - } else if script.is_v0_p2wpkh() { - self.sign_p2wpkh_input(idx, &utxo_pub_key_vec[idx], &mut tx_to_sign)?; - } else if script.is_v1_p2tr() { - self.sign_p2tr_input( - idx, - &utxo_pub_key_vec[idx], - &mut tx_to_sign, - SchnorrSighashType::Default, - )?; + self.calc_tx_hash(&mut tx_to_sign, &btc_version)?; + + //Compatible with Legacy and Nested Segwit transactions without upgrading btc apples + if btc_version.as_str() >= "1.6.00" { + self.tx_preview(&tx_to_sign, network)?; + for (idx, utxo) in self.unspents.iter().enumerate() { + let script = Script::from_str(&utxo.script_pubkey)?; + if script.is_p2pkh() { + self.sign_p2pkh_input(idx, &utxo_pub_key_vec[idx], &mut tx_to_sign)?; + } else if script.is_p2sh() { + self.sign_p2sh_nested_p2wpkh_input( + idx, + &utxo_pub_key_vec[idx], + &mut tx_to_sign, + )?; + } else if script.is_v0_p2wpkh() { + self.sign_p2wpkh_input(idx, &utxo_pub_key_vec[idx], &mut tx_to_sign)?; + } else if script.is_v1_p2tr() { + self.sign_p2tr_input( + idx, + &utxo_pub_key_vec[idx], + &mut tx_to_sign, + SchnorrSighashType::Default, + )?; + } else { + return Err(CoinError::InvalidUtxo.into()); + }; + } + } else { + for utxo in self.unspents.iter() { + let script = Script::from_str(&utxo.script_pubkey)?; + if !script.is_p2pkh() && !script.is_p2sh() { + return Err(CoinError::InvalidUtxo.into()); + } + } + let address_version = + get_address_version(network_convert(network), &self.to.to_string())?; + if VALID_ADDRESS_VERSIONS.contains(&address_version) { + if BTC_SEG_WIT_TYPE_P2WPKH.eq(&seg_wit.to_uppercase()) { + self.original_tx_preview(&tx_to_sign, network)?; + for (idx, _) in self.unspents.iter().enumerate() { + self.sign_p2sh_nested_p2wpkh_input( + idx, + &utxo_pub_key_vec[idx], + &mut tx_to_sign, + )?; + } + } else { + self.tx_preview(&tx_to_sign, network)?; + self.sign_p2pkh_inputs(&utxo_pub_key_vec, &mut tx_to_sign)?; + } } else { - return Err(CoinError::InvalidUtxo.into()); - }; + return Err(CoinError::InvalidAddress.into()); + } } let tx_bytes = serialize(&tx_to_sign); @@ -111,6 +153,76 @@ impl BtcTransaction { }) } + pub fn sign_p2pkh_inputs( + &self, + utxo_pub_key_vec: &Vec, + transaction: &mut Transaction, + ) -> Result<()> { + let mut lock_script_ver: Vec