From cfad8ae4c3be1a94290fe3c9bdff8e202cb52a9d Mon Sep 17 00:00:00 2001 From: Pedro Camboim <72642597+pedrxlz@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:50:43 -0300 Subject: [PATCH 1/3] feat: added getPublicKey method and enhanced tests for kos-web package (#92) --- packages/kos-web/src/wallet.rs | 201 ++++++++++++++++++++++++++------- 1 file changed, 163 insertions(+), 38 deletions(-) diff --git a/packages/kos-web/src/wallet.rs b/packages/kos-web/src/wallet.rs index 0b46425..9812a59 100644 --- a/packages/kos-web/src/wallet.rs +++ b/packages/kos-web/src/wallet.rs @@ -26,6 +26,7 @@ pub struct Wallet { chain: u32, account_type: AccountType, public_address: String, + public_key: String, index: Option, encrypted_data: Option>, mnemonic: Option, @@ -88,13 +89,14 @@ impl Wallet { .get_pbk(private_key.clone()) .map_err(|e| Error::WalletManager(format!("get public key: {}", e)))?; let address = chain - .get_address(public_key) + .get_address(public_key.clone()) .map_err(|e| Error::WalletManager(format!("get address: {}", e)))?; Ok(Wallet { chain: chain_id, account_type: AccountType::Mnemonic, public_address: address, + public_key: hex::encode(public_key), index: None, encrypted_data: None, private_key: Some(hex::encode(private_key)), @@ -136,7 +138,7 @@ impl Wallet { .ok_or_else(|| Error::WalletManager("Invalid chain".to_string()))?; let public_key = chain - .get_pbk(hex::decode(private_key_bytes.clone())?) + .get_pbk(private_key_bytes.clone()) .map_err(|e| Error::WalletManager(format!("get public key: {}", e)))?; let address = chain .get_address(public_key.clone()) @@ -147,6 +149,7 @@ impl Wallet { chain: chain_id, account_type: AccountType::PrivateKey, public_address: address, + public_key: hex::encode(public_key), index: None, encrypted_data: None, mnemonic: None, @@ -213,6 +216,12 @@ impl Wallet { self.public_address.clone() } + #[wasm_bindgen(js_name = "getPublicKey")] + /// get wallet public key + /// returns hex encoded public key + pub fn get_public_key(&self) -> String { + self.public_key.clone() + } #[wasm_bindgen(js_name = "getPath")] /// get wallet path if wallet is created from mnemonic pub fn get_path(&self) -> String { @@ -232,6 +241,7 @@ impl Wallet { #[wasm_bindgen(js_name = "getPrivateKey")] /// get wallet private key + /// returns hex encoded private key pub fn get_private_key(&self) -> String { match self.private_key { Some(ref pk) => pk.clone(), @@ -312,54 +322,169 @@ mod tests { use super::*; use kos::chains::get_chain_by_base_id; + const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const TEST_PRIVATE_KEY: &str = + "8734062c1158f26a3ca8a4a0da87b527a7c168653f7f4c77045e5cf571497d9d"; + const TEST_PUBLIC_KEY: &str = + "e41b323a571fd955e09cd41660ff4465c3f44693c87f2faea4a0fc408727c8ea"; + #[test] - fn test_export_import() { - let chain = get_chain_by_base_id(38).unwrap(); - - // create wallet - let w1 = Wallet::from_mnemonic( - 38, - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), - chain.get_path(0, false), - None, - ).unwrap(); - - // check if secret keys restored + fn test_wallet_from_mnemonic() { + let chain_id = 38; + let chain = get_chain_by_base_id(chain_id).unwrap(); + let path = chain.get_path(0, false); + + let wallet = + Wallet::from_mnemonic(chain_id, TEST_MNEMONIC.to_string(), path.clone(), None).unwrap(); + + assert_eq!(wallet.get_chain(), chain_id); + assert_eq!(wallet.get_account_type(), AccountType::Mnemonic); + assert_eq!(wallet.get_private_key(), TEST_PRIVATE_KEY); assert_eq!( - w1.get_private_key(), - "8734062c1158f26a3ca8a4a0da87b527a7c168653f7f4c77045e5cf571497d9d" + wallet.get_public_key(), + "e41b323a571fd955e09cd41660ff4465c3f44693c87f2faea4a0fc408727c8ea" ); - assert_eq!(w1.get_mnemonic(), "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"); - assert_eq!(w1.get_path(), "m/44'/690'/0'/0'/0'"); + assert_eq!(wallet.get_path(), path); + assert_eq!(wallet.get_mnemonic(), TEST_MNEMONIC); } #[test] - fn test_sign_transaction() { - let chain = get_chain_by_base_id(2).unwrap(); + fn test_wallet_from_mnemonic_with_password() { + let chain_id = 38; + let chain = get_chain_by_base_id(chain_id).unwrap(); + let path = chain.get_path(0, false); + let password = Some("mysecretpassword".to_string()); + + let wallet = + Wallet::from_mnemonic(chain_id, TEST_MNEMONIC.to_string(), path, password).unwrap(); - let w1 = Wallet::from_mnemonic( - 2, - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), - chain.get_path(0, false), - None, - ).unwrap(); - - let sign_options = TransactionChainOptions::new_bitcoin_sign_options( - vec![5000, 10000], - vec![ - "ABRUbV+OhmQeTR7sW5FVpUDZUyReSg==".to_string(), - "ABRUbV+OhmQeTR7sW5FVpUDZUyReSg==".to_string(), - ], + assert_eq!(wallet.get_account_type(), AccountType::Mnemonic); + assert_eq!(wallet.get_private_key().len(), 64); // Should be valid hex + } + + #[test] + fn test_wallet_from_mnemonic_index() { + let chain_id = 38; + let index = 5; + + let path_options = PathOptions { + index, + is_legacy: Some(false), + }; + + let wallet = + Wallet::from_mnemonic_index(chain_id, TEST_MNEMONIC.to_string(), &path_options, None) + .unwrap(); + + assert_eq!(wallet.get_index().unwrap(), index); + assert_eq!(wallet.get_account_type(), AccountType::Mnemonic); + assert_eq!( + wallet.get_private_key(), + "384f7222481134ed0b48416f986bc6c3660867340ef80fadd72db3388feafa8d" ); + assert_eq!( + wallet.get_public_key(), + "b94cd4566b6e6f18128e833b5d8ce50d5f11c0b816223f0210b552fa5c04979c" + ); + assert!(wallet.get_path().contains(&index.to_string())); + } + + #[test] + fn test_wallet_from_private_key() { + let chain_id = 38; + + let wallet = Wallet::from_private_key(chain_id, TEST_PRIVATE_KEY.to_string()).unwrap(); + + assert_eq!(wallet.get_chain(), chain_id); + assert_eq!(wallet.get_account_type(), AccountType::PrivateKey); + assert_eq!(wallet.get_private_key(), TEST_PRIVATE_KEY); + assert_eq!(wallet.get_public_key(), TEST_PUBLIC_KEY); + assert!(wallet.get_mnemonic().is_empty()); + assert!(wallet.get_path().is_empty()); + } + + #[test] + fn test_invalid_private_key() { + let chain_id = 38; + let invalid_pk = "invalid_private_key"; + + let result = Wallet::from_private_key(chain_id, invalid_pk.to_string()); + assert!(result.is_err()); + } + + #[test] + fn test_sign_message() { + let chain_id = 38; + let message = b"Hello, World!"; + + let wallet = Wallet::from_private_key(chain_id, TEST_PRIVATE_KEY.to_string()).unwrap(); + + let signature = wallet.sign_message(message).unwrap(); + assert!(!signature.is_empty()); + } + + #[test] + fn test_multiple_chains() { + let test_chains = vec![2, 38, 60]; + + for chain_id in test_chains { + let chain = get_chain_by_base_id(chain_id).unwrap(); + let path = chain.get_path(0, false); - let raw_tx = hex::decode("0100000002badfa0606bc6a1738d8ddf951b1ebf9e87779934a5774b836668efb5a6d643970000000000fffffffffe60fbeb66791b10c765a207c900a08b2a9bd7ef21e1dd6e5b2ef1e9d686e5230000000000ffffffff028813000000000000160014e4132ab9175345e24b344f50e6d6764a651a89e6c21f000000000000160014546d5f8e86641e4d1eec5b9155a540d953245e4a00000000").unwrap(); + let wallet = + Wallet::from_mnemonic(chain_id, TEST_MNEMONIC.to_string(), path, None).unwrap(); - let signed_tx = w1.sign(&raw_tx, Some(sign_options)).unwrap(); + assert_eq!(wallet.get_chain(), chain_id); + assert!(!wallet.get_address().is_empty()); + } + } - assert_eq!(signed_tx.raw_data.len(), 372); + #[test] + fn test_invalid_chain() { + let invalid_chain_id = 9999; + let chain = get_chain_by_base_id(2).unwrap(); + let path = chain.get_path(0, false); + + let result = Wallet::from_mnemonic(invalid_chain_id, TEST_MNEMONIC.to_string(), path, None); + + assert!(result.is_err()); + } + + #[test] + fn test_invalid_mnemonic() { + let chain_id = 38; + let invalid_mnemonic = "invalid mnemonic phrase"; + let chain = get_chain_by_base_id(chain_id).unwrap(); + let path = chain.get_path(0, false); + + let result = Wallet::from_mnemonic(chain_id, invalid_mnemonic.to_string(), path, None); + + assert!(result.is_err()); + } + + #[test] + fn test_readonly_wallet_operations() { + let chain_id = 38; + let public_address = "klv1fpwjz6w9sutqhfd4yf36zmd894de3h4ECt3"; + + let wallet = Wallet { + chain: chain_id, + account_type: AccountType::ReadOnly, + public_address: public_address.to_string(), + public_key: TEST_PUBLIC_KEY.to_string(), + index: None, + encrypted_data: None, + mnemonic: None, + private_key: None, + path: None, + }; - assert_eq!(signed_tx.tx_hash.len(), 32); + assert_eq!(wallet.get_address(), public_address); + assert!(wallet.get_private_key().is_empty()); + assert!(wallet.get_mnemonic().is_empty()); - assert_eq!(signed_tx.signature.len(), 32); + // Signing operations should fail + let message = b"test message"; + assert!(wallet.sign_message(message).is_err()); } } From 43739a6cb9a91bfecaeda63e11fc13d46e917f69 Mon Sep 17 00:00:00 2001 From: Pedro Camboim <72642597+pedrxlz@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:08:39 -0300 Subject: [PATCH 2/3] fix: browser klv tx (#93) --- packages/kos/src/chains/klv/mod.rs | 34 +++++++++++++++++++++++++-- packages/kos/src/chains/klv/models.rs | 4 +++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/kos/src/chains/klv/mod.rs b/packages/kos/src/chains/klv/mod.rs index 1d5c8ec..71daeb0 100644 --- a/packages/kos/src/chains/klv/mod.rs +++ b/packages/kos/src/chains/klv/mod.rs @@ -85,8 +85,13 @@ impl Chain for KLV { mut tx: Transaction, ) -> Result { let raw_tx = tx.raw_data; - let mut js_tx: models::Transaction = - tiny_json_rs::decode(String::from_utf8(raw_tx.clone())?)?; + + // Parse [] empty arrays to [""] to avoid decoding errors + let json = String::from_utf8(raw_tx.clone())?; + let parsed = json.replace("[]", "[\"\"]").as_bytes().to_vec(); + + let mut js_tx: models::Transaction = tiny_json_rs::decode(String::from_utf8(parsed)?)?; + let klv_tx = proto::Transaction::try_from(js_tx.clone()) .map_err(|_| ChainError::ProtoDecodeError)?; @@ -255,6 +260,31 @@ mod test { ); } + #[test] + fn test_sign_tx_3() { + let pvk = hex::decode("1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727") + .unwrap(); + + let json = r#"{"Signature":[],"RawData":{"Contract":[{"Parameter":{"value":"CiAErjVpczGsXwth+8y37LCGSr5O6tPlR9nduy2Np+8wyBIDS0xWGMCEPQ==","type_url":"type.googleapis.com\/proto.TransferContract"}}],"Nonce":580,"BandwidthFee":2000000,"Data":[""],"ChainID":"MTA4","Version":1,"Sender":"SqQFUzLDtZNzXexeY++BMVH5GTKLDMSxo72GoRqjZz0=","KAppFee":1000000}}"#; + + let raw_tx = json.as_bytes().to_vec(); + + let tx = crate::chains::Transaction { + raw_data: raw_tx, + tx_hash: Vec::new(), + signature: Vec::new(), + options: None, + }; + + let result_tx = crate::chains::klv::KLV {}.sign_tx(pvk, tx).unwrap(); + + assert_eq!( + result_tx.tx_hash, + hex::decode("cb2741a67bb2e84f21ac892c9b6577446955debe9c9ef40c1799d212e617a55f") + .unwrap() + ); + } + #[test] fn test_decode_klv_tx() { let raw_tx = hex::decode( diff --git a/packages/kos/src/chains/klv/models.rs b/packages/kos/src/chains/klv/models.rs index 8fced20..e09e819 100644 --- a/packages/kos/src/chains/klv/models.rs +++ b/packages/kos/src/chains/klv/models.rs @@ -176,7 +176,9 @@ impl TryFrom for proto::TxContract { type Error = ConversionError; fn try_from(value: chains::klv::models::TxContract) -> Result { - let contract_name = value.parameter.type_url.clone(); + // Remove escapes + let contract_name = value.parameter.type_url.replace("\\", ""); + //Remove the "type.googleapis.com/" prefix let contract_name = contract_name .strip_prefix("type.googleapis.com/proto.") From 5a6e05cc7f4ee0e89f806dfdf0addfa765520ebb Mon Sep 17 00:00:00 2001 From: Pedro Camboim <72642597+pedrxlz@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:25:08 -0300 Subject: [PATCH 3/3] chore: bump version (#94) --- Cargo.lock | 6 +++--- Cargo.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5cd97e6..11b9db4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1628,7 +1628,7 @@ dependencies = [ [[package]] name = "kos" -version = "0.2.3" +version = "0.2.4" dependencies = [ "aes", "aes-gcm", @@ -1670,7 +1670,7 @@ dependencies = [ [[package]] name = "kos-mobile" -version = "0.2.3" +version = "0.2.4" dependencies = [ "hex", "kos", @@ -1684,7 +1684,7 @@ dependencies = [ [[package]] name = "kos-web" -version = "0.2.3" +version = "0.2.4" dependencies = [ "enum_delegate", "enum_dispatch", diff --git a/Cargo.toml b/Cargo.toml index 5297596..8f4b2b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ homepage = "https://klever.org/" license = "Apache-2.0" repository = "https://github.com/kleverio/kos-rs" rust-version = "1.69.0" -version = "0.2.3" +version = "0.2.4" [workspace.dependencies] bech32 = "0.9.1" @@ -50,5 +50,5 @@ log = "0.4" lazy_static = "1.4.0" thiserror = "1.0" -kos = { version = "0.2.3", path = "./packages/kos", default-features = false } +kos = { version = "0.2.4", path = "./packages/kos", default-features = false } kos-mobile = { version = "0.1.0", path = "./packages/kos-mobile", default-features = false }