Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promote new version #95

Merged
merged 3 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 }
201 changes: 163 additions & 38 deletions packages/kos-web/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub struct Wallet {
chain: u32,
account_type: AccountType,
public_address: String,
public_key: String,
index: Option<u32>,
encrypted_data: Option<Vec<u8>>,
mnemonic: Option<String>,
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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())
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
Expand Down Expand Up @@ -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());
}
}
34 changes: 32 additions & 2 deletions packages/kos/src/chains/klv/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,13 @@ impl Chain for KLV {
mut tx: Transaction,
) -> Result<Transaction, ChainError> {
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)?;

Expand Down Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion packages/kos/src/chains/klv/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ impl TryFrom<chains::klv::models::TxContract> for proto::TxContract {
type Error = ConversionError;

fn try_from(value: chains::klv::models::TxContract) -> Result<Self, Self::Error> {
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.")
Expand Down
Loading