diff --git a/.gitmodules b/.gitmodules index 44977c9f..84a87d37 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "packages/kos/src/protos/tron"] - path = packages/kos/src/protos/tron +[submodule "packages/kos-codec/src/protos/tron"] + path = packages/kos-codec/src/protos/tron url = https://github.com/tronprotocol/protocol.git diff --git a/Cargo.lock b/Cargo.lock index 09eeb774..9dc78a77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,7 +235,7 @@ checksum = "83ad5da86c127751bc607c174d6c9fe9b85ef0889a9ca0c641735d77d4f98f26" dependencies = [ "alloy-sol-macro-input", "const-hex", - "heck 0.5.0", + "heck", "indexmap 2.9.0", "proc-macro-error2", "proc-macro2", @@ -253,7 +253,7 @@ checksum = "ba3d30f0d3f9ba3b7686f3ff1de9ee312647aac705604417a2f40c604f409a9e" dependencies = [ "const-hex", "dunce", - "heck 0.5.0", + "heck", "macro-string", "proc-macro2", "quote", @@ -1116,7 +1116,7 @@ version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.100", @@ -1830,16 +1830,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased-serde" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" -dependencies = [ - "serde", - "typeid", -] - [[package]] name = "errno" version = "0.3.11" @@ -2329,12 +2319,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -2864,15 +2848,6 @@ dependencies = [ "unic-langid", ] -[[package]] -name = "inventory" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" -dependencies = [ - "rustversion", -] - [[package]] name = "io_tee" version = "0.1.1" @@ -2972,14 +2947,13 @@ dependencies = [ [[package]] name = "kos" -version = "0.2.16" +version = "0.2.18" dependencies = [ "aes 0.8.4", "aes-gcm", "alloy-dyn-abi", "bech32 0.9.1", "bip39-dict", - "bitcoin", "blake2b-ref", "cbc", "cfb-mode", @@ -2987,20 +2961,12 @@ dependencies = [ "coins-bip39", "ed25519-dalek", "getrandom 0.2.15", - "heck 0.4.1", "hex", "hmac 0.12.1", "libsecp256k1", - "parity-scale-codec", "pbkdf2", "pem", "prost", - "prost-build", - "prost-types", - "prost-wkt", - "prost-wkt-build", - "prost-wkt-types", - "quote", "rand 0.8.5", "rand_core 0.6.4", "ripemd", @@ -3010,12 +2976,11 @@ dependencies = [ "serde_json", "sha2 0.10.8", "sha3", - "tiny-json-rs", ] [[package]] name = "kos-codec" -version = "0.2.16" +version = "0.2.18" dependencies = [ "bitcoin", "byteorder", @@ -3024,14 +2989,20 @@ dependencies = [ "hex", "kos", "pallas-primitives", + "parity-scale-codec", + "prost", + "prost-build", + "prost-types", + "rlp", "serde", "serde_json", + "tiny-json-rs", "xrpl-rust", ] [[package]] name = "kos-hardware" -version = "0.2.16" +version = "0.2.18" dependencies = [ "kos", "tiny-json-rs", @@ -3039,7 +3010,7 @@ dependencies = [ [[package]] name = "kos-mobile" -version = "0.2.16" +version = "0.2.18" dependencies = [ "anyhow", "bigdecimal", @@ -3059,7 +3030,7 @@ dependencies = [ [[package]] name = "kos-web" -version = "0.2.16" +version = "0.2.18" dependencies = [ "enum_delegate", "enum_dispatch", @@ -3906,7 +3877,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck 0.5.0", + "heck", "itertools 0.14.0", "log", "multimap", @@ -3942,52 +3913,6 @@ dependencies = [ "prost", ] -[[package]] -name = "prost-wkt" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d84e2bee181b04c2bac339f2bfe818c46a99750488cc6728ce4181d5aa8299" -dependencies = [ - "chrono", - "inventory", - "prost", - "serde", - "serde_derive", - "serde_json", - "typetag", -] - -[[package]] -name = "prost-wkt-build" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a669d5acbe719010c6f62a64e6d7d88fdedc1fe46e419747949ecb6312e9b14" -dependencies = [ - "heck 0.5.0", - "prost", - "prost-build", - "prost-types", - "quote", -] - -[[package]] -name = "prost-wkt-types" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ef068e9b82e654614b22e6b13699bd545b6c0e2e721736008b00b38aeb4f64" -dependencies = [ - "chrono", - "prost", - "prost-build", - "prost-types", - "prost-wkt", - "prost-wkt-build", - "regex", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "qr_code" version = "2.0.0" @@ -5006,7 +4931,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "rustversion", @@ -5469,42 +5394,12 @@ dependencies = [ "rustc-hash 1.1.0", ] -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" -[[package]] -name = "typetag" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f22b40dd7bfe8c14230cf9702081366421890435b2d625fa92b4acc4c3de6f" -dependencies = [ - "erased-serde", - "inventory", - "once_cell", - "serde", - "typetag-impl", -] - -[[package]] -name = "typetag-impl" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f5380909ffc31b4de4f4bdf96b877175a016aa2ca98cee39fcfd8c4d53d952" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "ucd-trie" version = "0.1.7" @@ -5610,7 +5505,7 @@ dependencies = [ "fs-err", "glob", "goblin", - "heck 0.5.0", + "heck", "once_cell", "paste", "serde", diff --git a/Cargo.toml b/Cargo.toml index dcddc30e..911b2266 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ homepage = "https://klever.org/" license = "Apache-2.0" repository = "https://github.com/kleverio/kos-rs" rust-version = "1.69.0" -version = "0.2.17" +version = "0.2.18" [workspace.dependencies] bech32 = "0.9.1" @@ -69,7 +69,7 @@ serde_json = "1.0" thiserror = "1.0" kos-mobile = { version = "0.1.0", path = "./packages/kos-mobile", default-features = false } ecies = { version = "0.2.7", default-features = false, features = ["pure"] } -kos = { version = "0.2.17", path = "./packages/kos", default-features = false, features = ["not-ksafe"] } +kos = { version = "0.2.18", path = "./packages/kos", default-features = false, features = ["not-ksafe"] } # lightning lwk_common = "0.9.0" diff --git a/packages/kos-codec/Cargo.toml b/packages/kos-codec/Cargo.toml index 4cf90f63..454a35fd 100644 --- a/packages/kos-codec/Cargo.toml +++ b/packages/kos-codec/Cargo.toml @@ -7,7 +7,7 @@ license.workspace = true repository.workspace = true rust-version.workspace = true version.workspace = true - +build = "build.rs" [lib] name = "kos_codec" @@ -24,3 +24,12 @@ byteorder = { version = "1.5.0" } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" pallas-primitives = "0.32.0" +rlp = { version = "0.5.0", default-features = false } +prost = "0.13.5" +tiny-json-rs = "0.2.5" +prost-types = { version = "0.13.5", default-features = false } +parity-scale-codec = { version = "3.6.9", default-features = false, features = ["derive"] } + + +[build-dependencies] +prost-build = "0.13.5" \ No newline at end of file diff --git a/packages/kos/build.rs b/packages/kos-codec/build.rs similarity index 100% rename from packages/kos/build.rs rename to packages/kos-codec/build.rs diff --git a/packages/kos-codec/src/chains/btc/mod.rs b/packages/kos-codec/src/chains/btc/mod.rs new file mode 100644 index 00000000..a83282b6 --- /dev/null +++ b/packages/kos-codec/src/chains/btc/mod.rs @@ -0,0 +1,279 @@ +use bitcoin::{ecdsa, secp256k1, sighash, Amount, Denomination, Psbt, ScriptBuf}; +use kos::chains::{ChainError, ChainOptions, Transaction}; + +pub fn encode_for_sign(mut transaction: Transaction) -> Result { + let options = transaction + .options + .clone() + .ok_or(ChainError::MissingOptions)?; + + let (prev_scripts, input_amounts) = match options { + ChainOptions::BTC { + prev_scripts, + input_amounts, + } => (prev_scripts, input_amounts), + _ => { + return Err(ChainError::InvalidOptions); + } + }; + + let bitcoin_transaction: bitcoin::Transaction = + bitcoin::consensus::deserialize(transaction.raw_data.as_ref()) + .map_err(|_| ChainError::DecodeRawTx)?; + + let mut psbt = + Psbt::from_unsigned_tx(bitcoin_transaction.clone()).map_err(|_| ChainError::DecodeRawTx)?; + + let values = input_amounts + .iter() + .map(|x| Amount::from_str_in(&x.to_string(), Denomination::Satoshi).unwrap()) + .collect::>(); + + for inp_idx in 0..psbt.inputs.len() { + let utxo = bitcoin::TxOut { + value: values[inp_idx], + script_pubkey: prev_scripts[inp_idx].clone().into(), + }; + psbt.inputs[inp_idx].witness_utxo = Some(utxo); + + // Add non_witness_utxo + psbt.inputs[inp_idx].non_witness_utxo = Some(bitcoin_transaction.clone()); + } + + let mut cache = sighash::SighashCache::new(bitcoin_transaction); + + // Compute tx_hash (sighash) for each input + let mut tx_hash_data = Vec::new(); + for inp_idx in 0..prev_scripts.len() { + let script_code = prev_scripts[inp_idx].clone(); + let script: ScriptBuf = script_code.into(); + + // Determine if it's a legacy or segwit transaction based on script type + if script.is_p2wpkh() { + // For SegWit (P2WPKH) transactions + let sig_hash = cache + .p2wpkh_signature_hash( + inp_idx, + &script, + values[inp_idx], + sighash::EcdsaSighashType::All, + ) + .map_err(|e| ChainError::InvalidTransaction(e.to_string()))?; + tx_hash_data.extend_from_slice(&sig_hash[..]); + } else if script.is_p2pkh() { + // For legacy (P2PKH) transactions + let sig_hash = cache + .legacy_signature_hash(inp_idx, &script, sighash::EcdsaSighashType::All.to_u32()) + .map_err(|e| ChainError::InvalidTransaction(e.to_string()))?; + tx_hash_data.extend_from_slice(&sig_hash[..]); + } else { + return Err(ChainError::InvalidTransaction( + "Unsupported script type. Only P2PKH and P2WPKH are supported.".to_string(), + )); + }; + } + + transaction.tx_hash = tx_hash_data; + + Ok(transaction) +} + +pub fn encode_for_broadcast( + mut transaction: Transaction, + public_key: String, +) -> Result { + let options = transaction + .options + .clone() + .ok_or(ChainError::MissingOptions)?; + + let (prev_scripts, input_amounts) = match options { + ChainOptions::BTC { + prev_scripts, + input_amounts, + } => (prev_scripts, input_amounts), + _ => { + return Err(ChainError::InvalidOptions); + } + }; + + let bitcoin_transaction: bitcoin::Transaction = + bitcoin::consensus::deserialize(transaction.raw_data.as_ref()) + .map_err(|_| ChainError::DecodeRawTx)?; + + let mut psbt = + Psbt::from_unsigned_tx(bitcoin_transaction.clone()).map_err(|_| ChainError::DecodeRawTx)?; + + let pub_key_bytes = hex::decode(public_key).map_err(|_| ChainError::InvalidPublicKey)?; + let bit_public_key = bitcoin::PublicKey::from_slice(pub_key_bytes.as_slice()) + .map_err(|_| ChainError::InvalidPublicKey)?; + + let values = input_amounts + .iter() + .map(|x| Amount::from_str_in(&x.to_string(), Denomination::Satoshi).unwrap()) + .collect::>(); + + for inp_idx in 0..psbt.inputs.len() { + let utxo = bitcoin::TxOut { + value: values[inp_idx], + script_pubkey: prev_scripts[inp_idx].clone().into(), + }; + psbt.inputs[inp_idx].witness_utxo = Some(utxo); + + // Add non_witness_utxo + psbt.inputs[inp_idx].non_witness_utxo = Some(bitcoin_transaction.clone()); + } + + // Process signature bytes from transaction.signature + let signatures = process_signatures(&transaction.signature)?; + + // Add signatures to PSBT and finalize + for (inp_idx, signature) in signatures.iter().enumerate().take(psbt.inputs.len()) { + // Create signature object from raw sig bytes + let sig_hash_ty = sighash::EcdsaSighashType::All; + let signature = ecdsa::Signature { + signature: secp256k1::ecdsa::Signature::from_compact(signature) + .map_err(|_| ChainError::InvalidSignature)?, + sighash_type: sig_hash_ty, + }; + + // Insert signature + psbt.inputs[inp_idx] + .partial_sigs + .insert(bit_public_key, signature); + } + + // Finalize PSBT + for (inp_idx, _) in prev_scripts.iter().enumerate().take(psbt.inputs.len()) { + let script_pubkey_bytes = prev_scripts[inp_idx].clone(); + let script_pubkey = bitcoin::Script::from_bytes(script_pubkey_bytes.as_slice()); + + // Check if it is a legacy or segwit transaction + let is_legacy = script_pubkey.is_p2pkh(); + let is_segwit = script_pubkey.is_p2wpkh(); + + if let Some((pubkey, sig)) = psbt.inputs[inp_idx].partial_sigs.first_key_value() { + if is_legacy { + let script_sig_builder = bitcoin::Script::builder() + .push_slice(sig.serialize()) + .push_slice(pubkey.inner.serialize()); + + let script = script_sig_builder.as_script(); + + psbt.inputs[inp_idx].final_script_sig = Some(ScriptBuf::from(script)); + } else if is_segwit { + let mut script_witness = bitcoin::Witness::new(); + script_witness.push(sig.to_vec()); + script_witness.push(pubkey.to_bytes()); + + psbt.inputs[inp_idx].final_script_witness = Some(script_witness); + } else { + // Unsupported script type + return Err(ChainError::UnsupportedScriptType); + } + } + } + + let signed_tx = psbt + .extract_tx() + .map_err(|e| ChainError::InvalidTransaction(e.to_string()))?; + + transaction.raw_data = bitcoin::consensus::encode::serialize(&signed_tx); + + let has_witness = signed_tx.input.iter().any(|x| !x.witness.is_empty()); + if has_witness { + transaction.signature = bitcoin::consensus::encode::serialize(&signed_tx.compute_wtxid()); + } else { + transaction.signature = bitcoin::consensus::encode::serialize(&signed_tx.compute_txid()); + } + + transaction.tx_hash = bitcoin::consensus::encode::serialize(&signed_tx.compute_txid()); + + Ok(transaction) +} + +// Helper function to process signature bytes from transaction.signaturez' +fn process_signatures(signature_data: &[u8]) -> Result>, ChainError> { + // Each sighash is 32 bytes, so we should have a multiple of 32 + if signature_data.len() % 64 != 0 { + return Err(ChainError::InvalidSignatureLength); + } + + let mut signatures = Vec::new(); + for chunk in signature_data.chunks(64) { + signatures.push(chunk.to_vec()); + } + + Ok(signatures) +} + +#[cfg(test)] +mod test { + use super::*; + use kos::chains::btc::BTC; + use kos::chains::Chain; + + #[test] + fn test_encode_for_sign() { + let raw_tx = hex::decode("0100000002badfa0606bc6a1738d8ddf951b1ebf9e87779934a5774b836668efb5a6d643970000000000fffffffffe60fbeb66791b10c765a207c900a08b2a9bd7ef21e1dd6e5b2ef1e9d686e5230000000000ffffffff028813000000000000160014e4132ab9175345e24b344f50e6d6764a651a89e6c21f000000000000160014546d5f8e86641e4d1eec5b9155a540d953245e4a00000000").unwrap(); + + let tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature: vec![], + options: Some(ChainOptions::BTC { + prev_scripts: vec![ + hex::decode("0014546d5f8e86641e4d1eec5b9155a540d953245e4a").unwrap(), + hex::decode("0014546d5f8e86641e4d1eec5b9155a540d953245e4a").unwrap(), + ], + input_amounts: vec![5000, 10000], + }), + }; + + let result = encode_for_sign(tx).unwrap(); + + assert_eq!(hex::encode(result.tx_hash), "685a9ec4cdaea214dfb48e75930c13aba9ed8980eb6bc93715ae16473deb49cf027756a5570d8871ea32a0367368833f90069803ac3344d40c8d8e7db3451e4b") + } + + #[test] + fn test_encode_for_broadcast() { + let raw_tx = hex::decode("0100000002badfa0606bc6a1738d8ddf951b1ebf9e87779934a5774b836668efb5a6d643970000000000fffffffffe60fbeb66791b10c765a207c900a08b2a9bd7ef21e1dd6e5b2ef1e9d686e5230000000000ffffffff028813000000000000160014e4132ab9175345e24b344f50e6d6764a651a89e6c21f000000000000160014546d5f8e86641e4d1eec5b9155a540d953245e4a00000000").unwrap(); + + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); + let btc = BTC::new(); + let seed = btc.mnemonic_to_seed(mnemonic, "".to_string()).unwrap(); + let path = btc.get_path(0, false); + let pvk = btc.derive(seed, path).unwrap(); + let pbk = btc.get_pbk(pvk.clone()).unwrap(); + let public_key_hex = hex::encode(pbk); + + let signature = vec![ + 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, 0x7b, 0x2c, 0xf5, 0x43, 0x58, + 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, 0x45, 0x77, 0x6b, 0x59, 0x90, + 0xa5, 0x49, 0xad, 0x54, 0x07, 0x75, 0x3e, 0x83, 0x02, 0x20, 0x57, 0x62, 0x41, 0xed, + 0x58, 0xfb, 0xd3, 0xcb, 0x2f, 0x74, 0x72, 0x60, 0x9b, 0xb6, 0x15, 0x73, 0x2a, 0x9b, + 0x9d, 0xca, 0x5c, 0x19, 0x97, 0x04, 0x88, 0xd9, + ]; + + let tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature, + options: Some(ChainOptions::BTC { + prev_scripts: vec![ + hex::decode("0014546d5f8e86641e4d1eec5b9155a540d953245e4a").unwrap(), + hex::decode("0014546d5f8e86641e4d1eec5b9155a540d953245e4a").unwrap(), + ], + input_amounts: vec![5000, 10000], + }), + }; + + let tx = encode_for_broadcast(tx, public_key_hex).unwrap(); + + assert_eq!(hex::encode(tx.raw_data), "01000000000102badfa0606bc6a1738d8ddf951b1ebf9e87779934a5774b836668efb5a6d643970000000000fffffffffe60fbeb66791b10c765a207c900a08b2a9bd7ef21e1dd6e5b2ef1e9d686e5230000000000ffffffff028813000000000000160014e4132ab9175345e24b344f50e6d6764a651a89e6c21f000000000000160014546d5f8e86641e4d1eec5b9155a540d953245e4a0247304402203045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54022007753e830220576241ed58fbd3cb2f7472609bb615732a9b9dca5c19970488d901210330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c0000000000"); + assert_eq!( + hex::encode(tx.signature), + "56decea9bc90063ad093a1bfe581fe4e8b50a60b4616877ea491cb8b9c4cec4e" + ); + } +} diff --git a/packages/kos-codec/src/chains/eth/mod.rs b/packages/kos-codec/src/chains/eth/mod.rs new file mode 100644 index 00000000..ae279f86 --- /dev/null +++ b/packages/kos-codec/src/chains/eth/mod.rs @@ -0,0 +1,119 @@ +mod models; + +use crate::chains::eth::models::EthereumTransaction; +use kos::chains::{ChainError, ChainOptions, Transaction}; +use kos::crypto::hash::keccak256_digest; + +pub fn encode_for_sign(mut transaction: Transaction) -> Result { + let mut eth_tx = EthereumTransaction::decode(&transaction.raw_data)?; + + let options = transaction + .options + .clone() + .ok_or(ChainError::MissingOptions)?; + + let chain_code = match options { + ChainOptions::EVM { chain_id } => chain_id, + _ => { + return Err(ChainError::InvalidOptions); + } + }; + + //Ensure empty signature + eth_tx.signature = None; + if eth_tx.transaction_type == models::TransactionType::Legacy { + eth_tx.chain_id = Some(chain_code as u64); + } + + let new_rlp = eth_tx.encode()?; + let to_sign = keccak256_digest(&new_rlp[..]); + + transaction.tx_hash = to_sign.to_vec(); + Ok(transaction) +} + +pub fn encode_for_broadcast(mut transaction: Transaction) -> Result { + let mut eth_tx = EthereumTransaction::decode(&transaction.raw_data)?; + + let mut signature_bytes: [u8; 65] = [0; 65]; + signature_bytes.copy_from_slice(&transaction.signature[..]); + eth_tx.signature = Some(signature_bytes); + let signed_rlp = eth_tx.encode()?; + transaction.raw_data = signed_rlp.clone(); + + Ok(transaction) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_legacy_tx() { + let raw_tx = hex::decode("b302f101819e84ae7937b285035f6cccc58252089498de4c83810b87f0e2cd92d80c9fac28c4ded4818568c696991f80c0808080").unwrap(); + + let mut tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature: vec![], + options: Some(ChainOptions::EVM { chain_id: 1 }), + }; + + let result = encode_for_sign(tx.clone()).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "5ed21ed1618c98b5b1814565d8d7a63ebc6425997c75b2b857d8692f0b73a64f" + ); + + tx.signature = vec![ + 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, 0x7b, 0x2c, 0xf5, 0x43, 0x58, + 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, 0x45, 0x77, 0x6b, 0x59, 0x90, + 0xa5, 0x49, 0xad, 0x54, 0x07, 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, + 0x7b, 0x2c, 0xf5, 0x43, 0x58, 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, + 0x45, 0x77, 0x6b, 0x59, 0x90, 0xa5, 0x49, 0xad, 0x54, + ]; + + let signed_tx = encode_for_broadcast(tx.clone()).unwrap(); + + assert_eq!(hex::encode(signed_tx.raw_data), "02f87101819e84ae7937b285035f6cccc58252089498de4c83810b87f0e2cd92d80c9fac28c4ded4818568c696991f80c054a054ad49a590596b77452a01141de380dd50945843f52c7b94718fd30021024530a0ad49a590596b77452a01141de380dd50945843f52c7b94718fd3002102453007"); + assert_eq!( + hex::encode(signed_tx.signature), + "3045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54" + ); + } + #[test] + fn test_london_tx() { + let raw_tx = hex::decode("b87602f8730182014f84147b7eeb85084ec9f83f8301450994dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb0000000000000000000000004cbeee256240c92a9ad920ea6f4d7df6466d2cdc000000000000000000000000000000000000000000000000000000000000000ac0808080").unwrap(); + + let mut tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature: vec![], + options: Some(ChainOptions::EVM { chain_id: 1 }), + }; + + let result = encode_for_sign(tx.clone()).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "8823151a6987f2625239f058e453f5850e3d800b31f1dd60951a7e36e0769c2e" + ); + + tx.signature = vec![ + 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, 0x7b, 0x2c, 0xf5, 0x43, 0x58, + 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, 0x45, 0x77, 0x6b, 0x59, 0x90, + 0xa5, 0x49, 0xad, 0x54, 0x07, 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, + 0x7b, 0x2c, 0xf5, 0x43, 0x58, 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, + 0x45, 0x77, 0x6b, 0x59, 0x90, 0xa5, 0x49, 0xad, 0x54, + ]; + + let signed_tx = encode_for_broadcast(tx.clone()).unwrap(); + + assert_eq!(hex::encode(signed_tx.raw_data), "02f8b30182014f84147b7eeb85084ec9f83f8301450994dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb0000000000000000000000004cbeee256240c92a9ad920ea6f4d7df6466d2cdc000000000000000000000000000000000000000000000000000000000000000ac054a054ad49a590596b77452a01141de380dd50945843f52c7b94718fd30021024530a0ad49a590596b77452a01141de380dd50945843f52c7b94718fd3002102453007"); + assert_eq!( + hex::encode(signed_tx.signature), + "3045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54" + ); + } +} diff --git a/packages/kos/src/chains/eth/models.rs b/packages/kos-codec/src/chains/eth/models.rs similarity index 98% rename from packages/kos/src/chains/eth/models.rs rename to packages/kos-codec/src/chains/eth/models.rs index 53fccabc..01b0999d 100644 --- a/packages/kos/src/chains/eth/models.rs +++ b/packages/kos-codec/src/chains/eth/models.rs @@ -1,10 +1,6 @@ -use crate::crypto::bignum::U256; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; +use kos::crypto::bignum::U256; use rlp::{DecoderError, Rlp, RlpStream}; -extern crate rlp; - #[derive(Debug, PartialEq)] pub enum TransactionType { Legacy, diff --git a/packages/kos-codec/src/chains/icp/mod.rs b/packages/kos-codec/src/chains/icp/mod.rs new file mode 100644 index 00000000..5f2a6be7 --- /dev/null +++ b/packages/kos-codec/src/chains/icp/mod.rs @@ -0,0 +1,75 @@ +use kos::chains::util::{byte_vectors_to_bytes, bytes_to_byte_vectors}; +use kos::chains::{ChainError, Transaction}; +use tiny_json_rs::serializer; + +pub fn encode_for_sign(mut transaction: Transaction) -> Result { + let hex = hex::decode(transaction.raw_data.clone()).map_err(|_| ChainError::DecodeRawTx)?; + let raw_data_str = String::from_utf8(hex).map_err(|_| ChainError::DecodeRawTx)?; + + let wrapped_data = format!("{{\"hashes\":{}}}", raw_data_str); + + #[derive(tiny_json_rs::Deserialize)] + struct HashContainer { + hashes: Vec, + } + + let container: HashContainer = + tiny_json_rs::decode(wrapped_data).map_err(|_| ChainError::DecodeHash)?; + + let icp_hashes = container.hashes; + + let mut hashes = Vec::new(); + + for hash_hex in icp_hashes { + hashes.push(hex::decode(hash_hex).unwrap()); + } + + transaction.tx_hash = byte_vectors_to_bytes(&hashes); + Ok(transaction) +} + +pub fn encode_for_broadcast(mut transaction: Transaction) -> Result { + let signatures = bytes_to_byte_vectors(transaction.signature)?; + + let mut signatures_vec = Vec::new(); + for signature in signatures { + signatures_vec.push(hex::encode(signature)); + } + + let signatures_json = tiny_json_rs::encode(signatures_vec); + transaction.signature = signatures_json.into_bytes(); + Ok(transaction) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_tx() { + let raw_tx = hex::decode("35623232333036313336333933363333333236343337333233363335333733313337333533363335333733333337333433353332333133373635333533363336333433393636363633323331333233383333333636363330363133323332333633323336333736363333333036333631333033303635333836353334363633323635333833333334363236313336363333303634333236343338333233343332333733323336333933303337333033303232326332323330363133363339333633333332363433373332333633353337333133373335333633353337333333373334333136323331333433303632363236343337333633353337333136333330363133333334333333333331333333313338333533343333333736323635333836323338363236333334363433353635333233383339333833303331363136353631333133373635363133303336363136363332333036343331363533393337333532323564").unwrap(); + + let tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature: vec![], + options: None, + }; + + let mut result = encode_for_sign(tx.clone()).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash.clone()), + "020000002b0000000a69632d726571756573745217e56649ff212836f0a226267f30ca00e8e4f2e834ba6c0d2d8242726907002b0000000a69632d726571756573741b140bbd76571c0a343313185437be8b8bc4d5e289801aea17ea06af20d1e975" + ); + + result.signature = hex::decode("0200000040000000cfb3e72d741521a803a6a3769864413eef9500dfb5fb488d68b84066f8643785a69da83e2ec4c936e8408272ad96d1d461d4f91a26dd9fb43d21f9130a75b9064000000097ca0c2eef5673ee0528b3afa468cfd2bd3384b1dd98d3e4c9171855bed8b915c8b971ab8a842b5fb8fc78fdb7ca819753f355229d1fc0d57c3709e0615c0504").unwrap(); + + let result = encode_for_broadcast(result.clone()).unwrap(); + + assert_eq!( + hex::encode(result.signature), + "5b226366623365373264373431353231613830336136613337363938363434313365656639353030646662356662343838643638623834303636663836343337383561363964613833653265633463393336653834303832373261643936643164343631643466393161323664643966623433643231663931333061373562393036222c223937636130633265656635363733656530353238623361666134363863666432626433333834623164643938643365346339313731383535626564386239313563386239373161623861383432623566623866633738666462376361383139373533663335353232396431666330643537633337303965303631356330353034225d" + ); + } +} diff --git a/packages/kos-codec/src/chains/klv/mod.rs b/packages/kos-codec/src/chains/klv/mod.rs new file mode 100644 index 00000000..b7ae4abd --- /dev/null +++ b/packages/kos-codec/src/chains/klv/mod.rs @@ -0,0 +1,95 @@ +mod models; + +use crate::protos::generated::klv::proto; +use kos::chains::{ChainError, Transaction}; +use kos::crypto::base64::simple_base64_encode; +use kos::crypto::hash::blake2b_digest; +use prost::Message; + +pub fn encode_for_sign(mut transaction: Transaction) -> Result { + let raw_tx = transaction.raw_data.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 js_tx: models::Transaction = tiny_json_rs::decode(String::from_utf8(parsed)?) + .map_err(|_| ChainError::InvalidTransaction("Failed to decode transaction".to_string()))?; + + let klv_tx = + proto::Transaction::try_from(js_tx.clone()).map_err(|_| ChainError::ProtoDecodeError)?; + + let raw_data = klv_tx + .raw_data + .clone() + .ok_or(ChainError::ProtoDecodeError)?; + let mut tx_raw = Vec::with_capacity(raw_data.encoded_len()); + raw_data.encode(&mut tx_raw)?; + transaction.tx_hash = blake2b_digest(&tx_raw).to_vec(); + + Ok(transaction) +} + +pub fn encode_for_broadcast(mut transaction: Transaction) -> Result { + let raw_tx = transaction.raw_data.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)?) + .map_err(|_| ChainError::InvalidTransaction("Failed to decode transaction".to_string()))?; + + js_tx.signature = Some(Vec::from([simple_base64_encode(&transaction.signature)])); + + transaction.raw_data = tiny_json_rs::encode(js_tx).into_bytes(); + + Ok(transaction) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_encode_for_sign() { + let raw_tx ="{\"RawData\":{\"BandwidthFee\":1000000,\"ChainID\":\"MTAwNDIw\",\"Contract\":[{\"Parameter\":{\"type_url\":\"type.googleapis.com/proto.TransferContract\",\"value\":\"CiAysyg0Aj8xj/rr5XGU6iJ+ATI29mnRHS0W0BrC1vz0CBgK\"}}],\"KAppFee\":500000,\"Nonce\":39,\"Sender\":\"5BsyOlcf2VXgnNQWYP9EZcP0RpPIfy+upKD8QIcnyOo=\",\"Version\":1}}".as_bytes().to_vec(); + + let tx = Transaction { + raw_data: raw_tx.clone(), + tx_hash: vec![], + signature: vec![], + options: None, + }; + + let result = encode_for_sign(tx).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "1e61c51f0d230f4855dc9b8935b47b9019887baf02be75d364a4068083833c15" + ); + } + + #[test] + fn test_encode_for_broadcast() { + let raw_tx ="{\"RawData\":{\"BandwidthFee\":1000000,\"ChainID\":\"MTAwNDIw\",\"Contract\":[{\"Parameter\":{\"type_url\":\"type.googleapis.com/proto.TransferContract\",\"value\":\"CiAysyg0Aj8xj/rr5XGU6iJ+ATI29mnRHS0W0BrC1vz0CBgK\"}}],\"KAppFee\":500000,\"Nonce\":39,\"Sender\":\"5BsyOlcf2VXgnNQWYP9EZcP0RpPIfy+upKD8QIcnyOo=\",\"Version\":1}}".as_bytes().to_vec(); + + let tx = Transaction { + raw_data: raw_tx.clone(), + tx_hash: vec![], + signature: vec![ + 2, 0, 0, 0, 72, 0, 0, 0, 48, 69, 34, 1, 9, 150, 43, 40, 55, 79, 163, 209, 160, 3, + 67, 48, 254, 119, 69, 171, 2, 219, 7, 205, 55, 100, 73, 230, 77, 110, 109, + ], + options: None, + }; + + let result = encode_for_broadcast(tx).unwrap(); + + assert_eq!(hex::encode(result.raw_data), "7b22426c6f636b223a6e756c6c2c2252617744617461223a7b2242616e647769647468466565223a313030303030302c22436861696e4944223a224d5441774e444977222c22436f6e7472616374223a5b7b22506172616d65746572223a7b22747970655f75726c223a22747970652e676f6f676c65617069732e636f6d2f70726f746f2e5472616e73666572436f6e7472616374222c2276616c7565223a224369417973796730416a38786a2f72723558475536694a2b41544932396d6e52485330573042724331767a304342674b227d2c2254797065223a6e756c6c7d5d2c2244617461223a6e756c6c2c224b417070466565223a3530303030302c224b4441466565223a6e756c6c2c224e6f6e6365223a33392c225065726d697373696f6e4944223a6e756c6c2c2253656e646572223a22354273794f6c6366325658676e4e5157595039455a6350305270504966792b75704b44385149636e794f6f3d222c2256657273696f6e223a317d2c225265636569707473223a6e756c6c2c22526573756c74223a6e756c6c2c22526573756c74436f6465223a6e756c6c2c225369676e6174757265223a5b2241674141414567414141417752534942435a59724b4464506f39476741304d772f6e644671774c62423830335a456e6d54573574225d7d"); + assert_eq!( + hex::encode(result.signature), + "02000000480000003045220109962b28374fa3d1a0034330fe7745ab02db07cd376449e64d6e6d" + ); + } +} diff --git a/packages/kos/src/chains/klv/models.rs b/packages/kos-codec/src/chains/klv/models.rs similarity index 83% rename from packages/kos/src/chains/klv/models.rs rename to packages/kos-codec/src/chains/klv/models.rs index a32aa45c..2454c316 100644 --- a/packages/kos/src/chains/klv/models.rs +++ b/packages/kos-codec/src/chains/klv/models.rs @@ -1,23 +1,19 @@ -use crate::alloc::borrow::ToOwned; -use crate::alloc::string::ToString; -use crate::crypto::base64::simple_base64_decode; use crate::protos::generated::klv::proto; use crate::protos::generated::klv::proto::tx_contract::ContractType; -use crate::{chains, protos}; -use alloc::format; -use alloc::string::String; -use alloc::vec::Vec; use tiny_json_rs::mapper; use tiny_json_rs::serializer; use tiny_json_rs::Deserialize; use tiny_json_rs::Serialize; +use crate::protos; +use kos::crypto::base64::simple_base64_decode; + #[derive(Serialize, Deserialize)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq)] pub struct Transaction { #[Rename = "RawData"] - pub raw_data: ::core::option::Option, + pub raw_data: Option, #[Rename = "Signature"] pub signature: Option>, //Base64 encoded #[Rename = "Result"] @@ -37,11 +33,11 @@ pub struct Raw { #[Rename = "Sender"] pub sender: String, #[Rename = "Contract"] - pub contract: ::prost::alloc::vec::Vec, + pub contract: Vec, #[Rename = "PermissionID"] pub permission_id: Option, #[Rename = "Data"] - pub data: Option>, + pub data: Option>, #[Rename = "KAppFee"] // Use this to match the exact JSON field name for this field pub k_app_fee: Option, #[Rename = "BandwidthFee"] // Use this to match the exact JSON field name for this field @@ -81,21 +77,22 @@ pub struct KdaFee { #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct Receipt { #[Rename = "Data"] - pub data: ::prost::alloc::vec::Vec, + pub data: Vec, } #[derive(Debug)] +#[allow(dead_code)] pub enum ConversionError { InvalidData(&'static str), Base64Error, // You can add more error types as needed for detailed error handling } -impl TryFrom for proto::Transaction { +impl TryFrom for proto::Transaction { type Error = ConversionError; #[allow(clippy::needless_update)] - fn try_from(value: chains::klv::models::Transaction) -> Result { + fn try_from(value: Transaction) -> Result { let raw_data = match value.raw_data { Some(raw) => Some(proto::transaction::Raw::try_from(raw)?), None => None, @@ -129,11 +126,11 @@ impl TryFrom for proto::Transaction { } } -impl TryFrom for proto::transaction::Raw { +impl TryFrom for proto::transaction::Raw { type Error = ConversionError; #[allow(clippy::needless_update)] - fn try_from(value: chains::klv::models::Raw) -> Result { + fn try_from(value: Raw) -> Result { let contracts = value .contract .into_iter() @@ -172,10 +169,10 @@ impl TryFrom for proto::transaction::Raw { } } -impl TryFrom for proto::TxContract { +impl TryFrom for proto::TxContract { type Error = ConversionError; - fn try_from(value: chains::klv::models::TxContract) -> Result { + fn try_from(value: TxContract) -> Result { // Remove escapes let contract_name = value.parameter.type_url.replace("\\", ""); @@ -196,11 +193,11 @@ impl TryFrom for proto::TxContract { } } -impl TryFrom for protos::Any { +impl TryFrom for protos::Any { type Error = ConversionError; #[allow(clippy::needless_update)] - fn try_from(value: chains::klv::models::Parameter) -> Result { + fn try_from(value: Parameter) -> Result { let proto_parameter = protos::Any { type_url: value.type_url, value: simple_base64_decode(&value.value.unwrap_or_default()) @@ -212,11 +209,11 @@ impl TryFrom for protos::Any { } } -impl TryFrom for proto::transaction::KdaFee { +impl TryFrom for proto::transaction::KdaFee { type Error = ConversionError; #[allow(clippy::needless_update)] - fn try_from(value: chains::klv::models::KdaFee) -> Result { + fn try_from(value: KdaFee) -> Result { let kda_bytes = simple_base64_decode(&value.kda).map_err(|_| ConversionError::Base64Error)?; @@ -230,11 +227,11 @@ impl TryFrom for proto::transaction::KdaFee { } } -impl TryFrom for proto::transaction::Receipt { +impl TryFrom for proto::transaction::Receipt { type Error = ConversionError; #[allow(clippy::needless_update)] - fn try_from(value: chains::klv::models::Receipt) -> Result { + fn try_from(value: Receipt) -> Result { let data = value .data .into_iter() diff --git a/packages/kos-codec/src/chains/mod.rs b/packages/kos-codec/src/chains/mod.rs index b9abc622..9a0e8a4f 100644 --- a/packages/kos-codec/src/chains/mod.rs +++ b/packages/kos-codec/src/chains/mod.rs @@ -2,4 +2,11 @@ pub mod ada; pub mod apt; pub mod atom; pub mod bch; +pub mod btc; +pub mod eth; +pub mod icp; +pub mod klv; +pub mod sol; +pub mod substrate; +pub mod trx; pub mod xrp; diff --git a/packages/kos-codec/src/chains/sol/mod.rs b/packages/kos-codec/src/chains/sol/mod.rs new file mode 100644 index 00000000..d4388761 --- /dev/null +++ b/packages/kos-codec/src/chains/sol/mod.rs @@ -0,0 +1,93 @@ +use crate::chains::sol::models::SolanaTransaction; +use kos::chains::{ChainError, Transaction}; + +mod models; + +pub fn encode_for_sign(mut transaction: Transaction) -> Result { + let sol_tx = SolanaTransaction::decode(&transaction.raw_data)?; + + if sol_tx.message.header.num_required_signatures as usize != 1 { + return Err(ChainError::InvalidTransactionHeader); + } + if sol_tx.message.account_keys.is_empty() { + return Err(ChainError::InvalidAccountLength); + } + if sol_tx.message.recent_blockhash.iter().all(|&x| x == 0) + || sol_tx.message.recent_blockhash.iter().all(|&x| x == 1) + { + return Err(ChainError::InvalidBlockhash); + } + + transaction.tx_hash = sol_tx.message.encode()?; + + Ok(transaction) +} + +pub fn encode_for_broadcast(mut transaction: Transaction) -> Result { + let mut sol_tx = SolanaTransaction::decode(&transaction.raw_data)?; + + if transaction.signature.len() != 64 { + return Err(ChainError::InvalidSignatureLength); + } + sol_tx.signatures = vec![transaction.signature.clone()]; + + transaction.tx_hash = sol_tx.signatures[0].clone(); + + let signed_tx = sol_tx.encode()?; + + transaction.raw_data = signed_tx; + Ok(transaction) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_encode_for_sign() { + let raw_tx = hex::decode( + "00010000030101010101010101010101010101010101010101010101010101010101010101020202020202020202020202020202020202020202020202020202020202020203030303030303030303030303030303030303030303030303030303030303032a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a01020200010c020000006400000000000000" + ).unwrap(); + let tx = Transaction { + raw_data: raw_tx.clone(), + tx_hash: vec![], + signature: vec![], + options: None, + }; + + let result = encode_for_sign(tx).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "010000030101010101010101010101010101010101010101010101010101010101010101020202020202020202020202020202020202020202020202020202020202020203030303030303030303030303030303030303030303030303030303030303032a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a01020200010c020000006400000000000000" + ); + } + + #[test] + fn test_encode_for_broadcast() { + let raw_tx = hex::decode( + "00010000030101010101010101010101010101010101010101010101010101010101010101020202020202020202020202020202020202020202020202020202020202020203030303030303030303030303030303030303030303030303030303030303032a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a01020200010c020000006400000000000000" + ).unwrap(); + let tx = Transaction { + raw_data: raw_tx.clone(), + tx_hash: vec![], + signature: vec![ + // 64 bytes of signature + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, + 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + ], + options: None, + }; + + let result = encode_for_broadcast(tx).unwrap(); + + assert_eq!(hex::encode(result.raw_data), "01000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f010000030101010101010101010101010101010101010101010101010101010101010101020202020202020202020202020202020202020202020202020202020202020203030303030303030303030303030303030303030303030303030303030303032a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a01020200010c020000006400000000000000"); + assert_eq!( + hex::encode(result.signature), + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" + ); + } +} diff --git a/packages/kos/src/chains/sol/models.rs b/packages/kos-codec/src/chains/sol/models.rs similarity index 98% rename from packages/kos/src/chains/sol/models.rs rename to packages/kos-codec/src/chains/sol/models.rs index 9f97acde..4ae2e70b 100644 --- a/packages/kos/src/chains/sol/models.rs +++ b/packages/kos-codec/src/chains/sol/models.rs @@ -1,8 +1,3 @@ -use alloc::format; -use alloc::string::{String, ToString}; -use alloc::vec; -use alloc::vec::Vec; - #[derive(Debug)] pub struct MessageHeader { pub num_required_signatures: u8, diff --git a/packages/kos-codec/src/chains/substrate/mod.rs b/packages/kos-codec/src/chains/substrate/mod.rs new file mode 100644 index 00000000..d2632c9b --- /dev/null +++ b/packages/kos-codec/src/chains/substrate/mod.rs @@ -0,0 +1,480 @@ +mod models; + +use crate::chains::substrate::models::ExtrinsicPayload; +use crate::KosCodedAccount; +use kos::chains::{ChainError, ChainOptions, Transaction}; +use kos::crypto::hash::blake2b_digest; + +pub fn encode_for_sign( + mut transaction: Transaction, + account: KosCodedAccount, +) -> Result { + let extrinsic = unwrap_extrinsic(transaction.clone(), account.clone())?; + + let tx_hash = { + let full_unsigned_payload_scale_bytes = extrinsic.to_bytes(); + + // If payload is longer than 256 bytes, we hash it and sign the hash instead: + if full_unsigned_payload_scale_bytes.len() > 256 { + blake2b_digest(&full_unsigned_payload_scale_bytes).to_vec() + } else { + full_unsigned_payload_scale_bytes + } + }; + + transaction.tx_hash = tx_hash; + + Ok(transaction) +} + +pub fn encode_for_broadcast( + mut transaction: Transaction, + account: KosCodedAccount, +) -> Result { + let extrinsic = unwrap_extrinsic(transaction.clone(), account.clone())?; + + let public_key: [u8; 32] = hex::decode(account.public_key) + .unwrap() + .try_into() + .map_err(|_| ChainError::InvalidPublicKey)?; + + transaction.raw_data = extrinsic.encode_with_signature(&public_key, &transaction.signature); + Ok(transaction) +} + +fn unwrap_extrinsic( + transaction: Transaction, + account: KosCodedAccount, +) -> Result { + let options = transaction + .options + .clone() + .ok_or(ChainError::MissingOptions)?; + + match options { + ChainOptions::SUBSTRATE { + call, + era, + nonce, + tip, + block_hash, + genesis_hash, + spec_version, + transaction_version, + app_id, + } => { + let genesis_hash: [u8; 32] = genesis_hash + .as_slice() + .try_into() + .map_err(|_| ChainError::InvalidOptions)?; + + let block_hash: [u8; 32] = block_hash + .as_slice() + .try_into() + .map_err(|_| ChainError::InvalidOptions)?; + + // Other chains may have different requirements for mode and metadata_hash + let (mode, metadata_hash) = match account.chain_id { + 29 => (None, None), + _ => (Some(0u8), Some(0u8)), + }; + + Ok(ExtrinsicPayload { + call, + era, + nonce, + tip, + mode, + spec_version, + transaction_version, + genesis_hash, + block_hash, + metadata_hash, + app_id, + }) + } + _ => Err(ChainError::InvalidOptions), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_tx_dot() { + let raw_tx = hex::decode("b302f101819e84ae7937b285035f6cccc58252089498de4c83810b87f0e2cd92d80c9fac28c4ded4818568c696991f80c0808080").unwrap(); + + let mut tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature: vec![], + options: Some(ChainOptions::SUBSTRATE { + call: hex::decode( + "0503000c2441b8cedbfc7a2edc0968b9a535819969d3e9e0998680babb5827287fc07004", + ) + .unwrap(), + era: hex::decode("d501").unwrap(), + nonce: 27, + tip: 0, + block_hash: hex::decode( + "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + ) + .unwrap(), + genesis_hash: hex::decode( + "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + ) + .unwrap(), + spec_version: 1003004, + transaction_version: 26, + app_id: None, + }), + }; + + let acc = KosCodedAccount { + public_key: "66933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972" + .to_string(), + chain_id: 21, + address: "13KVd4f2a4S5pLp4gTTFezyXdPWx27vQ9vS6xBXJ9yWVd7xo".to_string(), + }; + + let result = encode_for_sign(tx.clone(), acc.clone()).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "0503000c2441b8cedbfc7a2edc0968b9a535819969d3e9e0998680babb5827287fc07004d5016c0000fc4d0f001a00000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c391b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c300" + ); + + tx.signature = vec![ + 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, 0x7b, 0x2c, 0xf5, 0x43, 0x58, + 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, 0x45, 0x77, 0x6b, 0x59, 0x90, + 0xa5, 0x49, 0xad, 0x54, 0x07, 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, + 0x7b, 0x2c, 0xf5, 0x43, 0x58, 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, + 0x45, 0x77, 0x6b, 0x59, 0x90, 0xa5, 0x49, 0xad, 0x54, + ]; + + let signed_tx = encode_for_broadcast(tx.clone(), acc).unwrap(); + + assert_eq!(hex::encode(signed_tx.raw_data), "3502840066933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972013045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54d5016c00000503000c2441b8cedbfc7a2edc0968b9a535819969d3e9e0998680babb5827287fc07004"); + assert_eq!( + hex::encode(signed_tx.signature), + "3045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54" + ); + } + + #[test] + fn test_tx_ksm() { + let raw_tx = hex::decode("b302f101819e84ae7937b285035f6cccc58252089498de4c83810b87f0e2cd92d80c9fac28c4ded4818568c696991f80c0808080").unwrap(); + + let mut tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature: vec![], + options: Some(ChainOptions::SUBSTRATE { + call: hex::decode( + "0403004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904", + ) + .unwrap(), + era: hex::decode("4502").unwrap(), + nonce: 87, + tip: 0, + block_hash: hex::decode( + "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", + ) + .unwrap(), + genesis_hash: hex::decode( + "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", + ) + .unwrap(), + spec_version: 1003003, + transaction_version: 26, + app_id: None, + }), + }; + + let acc = KosCodedAccount { + chain_id: 27, + address: "Etp93jqLeBY8TczVXDJQoWNvMoY8VBSXoYNBYou5ghUBeC1".to_string(), + public_key: "66933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972" + .to_string(), + }; + + let result = encode_for_sign(tx.clone(), acc.clone()).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "0403004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd590445025d010000fb4d0f001a000000b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafeb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe00" + ); + + tx.signature = vec![ + 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, 0x7b, 0x2c, 0xf5, 0x43, 0x58, + 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, 0x45, 0x77, 0x6b, 0x59, 0x90, + 0xa5, 0x49, 0xad, 0x54, 0x07, 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, + 0x7b, 0x2c, 0xf5, 0x43, 0x58, 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, + 0x45, 0x77, 0x6b, 0x59, 0x90, 0xa5, 0x49, 0xad, 0x54, + ]; + + let signed_tx = encode_for_broadcast(tx.clone(), acc).unwrap(); + + assert_eq!(hex::encode(signed_tx.raw_data), "3902840066933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972013045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad5445025d0100000403004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904"); + assert_eq!( + hex::encode(signed_tx.signature), + "3045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54" + ); + } + #[test] + fn test_tx_avail() { + let raw_tx = hex::decode("b302f101819e84ae7937b285035f6cccc58252089498de4c83810b87f0e2cd92d80c9fac28c4ded4818568c696991f80c0808080").unwrap(); + let nonce = u32::from_str_radix("0x00000008".trim_start_matches("0x"), 16).unwrap(); + let spec_version = u32::from_str_radix("0x00000028".trim_start_matches("0x"), 16).unwrap(); + let transaction_version = + u32::from_str_radix("0x00000001".trim_start_matches("0x"), 16).unwrap(); + + let mut tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature: vec![], + options: Some(ChainOptions::SUBSTRATE { + call: hex::decode( + "0603004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904", + ) + .unwrap(), + era: hex::decode("b501").unwrap(), + nonce, + tip: 0, + block_hash: hex::decode( + "0e15fed86501da447cae3b7361fc14a087f309aeb751085d71a988aa4bb4a811", + ) + .unwrap(), + genesis_hash: hex::decode( + "b91746b45e0346cc2f815a520b9c6cb4d5c0902af848db0a80f85932d2e8276a", + ) + .unwrap(), + spec_version, + transaction_version, + app_id: Some(0), + }), + }; + + let acc = KosCodedAccount { + chain_id: 62, + address: "5EPCUjPxiHAcNooYipQFWr9NmmXJKpNG5RhcntXwbtUySrgH".to_string(), + public_key: "66933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972" + .to_string(), + }; + + let result = encode_for_sign(tx.clone(), acc.clone()).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "0603004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904b5012000002800000001000000b91746b45e0346cc2f815a520b9c6cb4d5c0902af848db0a80f85932d2e8276a0e15fed86501da447cae3b7361fc14a087f309aeb751085d71a988aa4bb4a811" + ); + + tx.signature = vec![ + 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, 0x7b, 0x2c, 0xf5, 0x43, 0x58, + 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, 0x45, 0x77, 0x6b, 0x59, 0x90, + 0xa5, 0x49, 0xad, 0x54, 0x07, 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, + 0x7b, 0x2c, 0xf5, 0x43, 0x58, 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, + 0x45, 0x77, 0x6b, 0x59, 0x90, 0xa5, 0x49, 0xad, 0x54, + ]; + + let signed_tx = encode_for_broadcast(tx.clone(), acc).unwrap(); + + assert_eq!(hex::encode(signed_tx.raw_data), "3502840066933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972013045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54b5012000000603004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904"); + assert_eq!( + hex::encode(signed_tx.signature), + "3045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54" + ); + } + + #[test] + fn test_tx_reef() { + let raw_tx = hex::decode("b302f101819e84ae7937b285035f6cccc58252089498de4c83810b87f0e2cd92d80c9fac28c4ded4818568c696991f80c0808080").unwrap(); + let nonce = 24; + let spec_version = 10; + let transaction_version = 2; + + let mut tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature: vec![], + options: Some(ChainOptions::SUBSTRATE { + call: hex::decode( + "0603002010065fd68017f85177f9acf9809ab6e359ec7631c9f972fe3ef9697af0931304", + ) + .unwrap(), + era: hex::decode("1503").unwrap(), + nonce, + tip: 0, + block_hash: hex::decode( + "567c2424bbef73128c80b319cec4fc6140e122b23ef22096f2be41a651cad76b", + ) + .unwrap(), + genesis_hash: hex::decode( + "7834781d38e4798d548e34ec947d19deea29df148a7bf32484b7b24dacf8d4b7", + ) + .unwrap(), + spec_version, + transaction_version, + app_id: None, + }), + }; + + let acc = KosCodedAccount { + chain_id: 29, + address: "5EPCUjPxiHAcNooYipQFWr9NmmXJKpNG5RhcntXwbtUySrgH".to_string(), + public_key: "66933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972" + .to_string(), + }; + + let result = encode_for_sign(tx.clone(), acc.clone()).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "0603002010065fd68017f85177f9acf9809ab6e359ec7631c9f972fe3ef9697af0931304150360000a000000020000007834781d38e4798d548e34ec947d19deea29df148a7bf32484b7b24dacf8d4b7567c2424bbef73128c80b319cec4fc6140e122b23ef22096f2be41a651cad76b" + ); + + tx.signature = vec![ + 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, 0x7b, 0x2c, 0xf5, 0x43, 0x58, + 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, 0x45, 0x77, 0x6b, 0x59, 0x90, + 0xa5, 0x49, 0xad, 0x54, 0x07, 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, + 0x7b, 0x2c, 0xf5, 0x43, 0x58, 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, + 0x45, 0x77, 0x6b, 0x59, 0x90, 0xa5, 0x49, 0xad, 0x54, + ]; + + let signed_tx = encode_for_broadcast(tx.clone(), acc).unwrap(); + + assert_eq!(hex::encode(signed_tx.raw_data), "3102840066933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972013045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54150360000603002010065fd68017f85177f9acf9809ab6e359ec7631c9f972fe3ef9697af0931304"); + assert_eq!( + hex::encode(signed_tx.signature), + "3045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54" + ); + } + + #[test] + fn test_tx_kar() { + let raw_tx = hex::decode("b302f101819e84ae7937b285035f6cccc58252089498de4c83810b87f0e2cd92d80c9fac28c4ded4818568c696991f80c0808080").unwrap(); + let nonce = 0; + let spec_version = 2280; + let transaction_version = 2; + + let mut tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature: vec![], + options: Some(ChainOptions::SUBSTRATE { + call: hex::decode( + "0a03006d6f646c6163612f696e6374000000000000000000000000000000000000000004", + ) + .unwrap(), + era: hex::decode("f502").unwrap(), + nonce, + tip: 0, + block_hash: hex::decode( + "4e29888d26fcdbdc19016d7d9ea2aa4f98e4a53a3cd1602008ba82def26eeb27", + ) + .unwrap(), + genesis_hash: hex::decode( + "baf5aabe40646d11f0ee8abbdc64f4a4b7674925cba08e4a05ff9ebed6e2126b", + ) + .unwrap(), + spec_version, + transaction_version, + app_id: None, + }), + }; + + let acc = KosCodedAccount { + chain_id: 41, + address: "qcmgzzFePRu4p3mviVSgD6voGfJTaxZfSs9sefhrpGPsejg".to_string(), + public_key: "66933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972" + .to_string(), + }; + + let result = encode_for_sign(tx.clone(), acc.clone()).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "0a03006d6f646c6163612f696e6374000000000000000000000000000000000000000004f502000000e808000002000000baf5aabe40646d11f0ee8abbdc64f4a4b7674925cba08e4a05ff9ebed6e2126b4e29888d26fcdbdc19016d7d9ea2aa4f98e4a53a3cd1602008ba82def26eeb2700" + ); + + tx.signature = vec![ + 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, 0x7b, 0x2c, 0xf5, 0x43, 0x58, + 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, 0x45, 0x77, 0x6b, 0x59, 0x90, + 0xa5, 0x49, 0xad, 0x54, 0x07, 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, + 0x7b, 0x2c, 0xf5, 0x43, 0x58, 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, + 0x45, 0x77, 0x6b, 0x59, 0x90, 0xa5, 0x49, 0xad, 0x54, + ]; + + let signed_tx = encode_for_broadcast(tx.clone(), acc).unwrap(); + + assert_eq!(hex::encode(signed_tx.raw_data), "3502840066933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972013045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54f5020000000a03006d6f646c6163612f696e6374000000000000000000000000000000000000000004"); + assert_eq!( + hex::encode(signed_tx.signature), + "3045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54" + ); + } + #[test] + fn test_tx_aca() { + let raw_tx = hex::decode("b302f101819e84ae7937b285035f6cccc58252089498de4c83810b87f0e2cd92d80c9fac28c4ded4818568c696991f80c0808080").unwrap(); + let nonce = 4981; + let spec_version = 2280; + let transaction_version = 3; + + let mut tx = Transaction { + raw_data: raw_tx, + tx_hash: vec![], + signature: vec![], + options: Some(ChainOptions::SUBSTRATE { + call: hex::decode( + "0a03006d6f646c6163612f6465786d000000000000000000000000000000000000000004", + ) + .unwrap(), + era: hex::decode("0503").unwrap(), + nonce, + tip: 0, + block_hash: hex::decode( + "c64067e6203771c6a0f0a8cbd1cdb710c2a9e453733f47b62014cb9d39220723", + ) + .unwrap(), + genesis_hash: hex::decode( + "fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c", + ) + .unwrap(), + spec_version, + transaction_version, + app_id: None, + }), + }; + + let acc = KosCodedAccount { + chain_id: 46, + address: "23C6Cz54QyBMNvrhjnFVS1dn6EwtZxDc3KyR71xJnXTNSDst".to_string(), + public_key: "66933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972" + .to_string(), + }; + + let result = encode_for_sign(tx.clone(), acc.clone()).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "0a03006d6f646c6163612f6465786d0000000000000000000000000000000000000000040503d54d0000e808000003000000fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64cc64067e6203771c6a0f0a8cbd1cdb710c2a9e453733f47b62014cb9d3922072300" + ); + + tx.signature = vec![ + 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, 0x7b, 0x2c, 0xf5, 0x43, 0x58, + 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, 0x45, 0x77, 0x6b, 0x59, 0x90, + 0xa5, 0x49, 0xad, 0x54, 0x07, 0x30, 0x45, 0x02, 0x21, 0x00, 0xd3, 0x8f, 0x71, 0x94, + 0x7b, 0x2c, 0xf5, 0x43, 0x58, 0x94, 0x50, 0xdd, 0x80, 0xe3, 0x1d, 0x14, 0x01, 0x2a, + 0x45, 0x77, 0x6b, 0x59, 0x90, 0xa5, 0x49, 0xad, 0x54, + ]; + + let signed_tx = encode_for_broadcast(tx.clone(), acc).unwrap(); + + assert_eq!(hex::encode(signed_tx.raw_data), "3902840066933bd1f37070ef87bd1198af3dacceb095237f803f3d32b173e6b425ed7972013045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad540503d54d00000a03006d6f646c6163612f6465786d000000000000000000000000000000000000000004"); + assert_eq!( + hex::encode(signed_tx.signature), + "3045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54073045022100d38f71947b2cf543589450dd80e31d14012a45776b5990a549ad54" + ); + } +} diff --git a/packages/kos-codec/src/chains/substrate/models.rs b/packages/kos-codec/src/chains/substrate/models.rs new file mode 100644 index 00000000..b7071830 --- /dev/null +++ b/packages/kos-codec/src/chains/substrate/models.rs @@ -0,0 +1,91 @@ +use parity_scale_codec::{Compact, Encode}; + +const SIGNED_FLAG: u8 = 0b1000_0000; +const TRANSACTION_VERSION: u8 = 4; +const PUBLIC_KEY_TYPE: u8 = 0x00; +const SIGNATURE_TYPE: u8 = 0x01; + +/// Represents the payload of a Substrate extrinsic (transaction) that will be signed. +/// This structure contains all the necessary fields required for transaction signing. +#[allow(dead_code)] +pub struct ExtrinsicPayload { + pub call: Vec, + pub era: Vec, + pub nonce: u32, + pub tip: u8, + pub mode: Option, + pub spec_version: u32, + pub transaction_version: u32, + pub genesis_hash: [u8; 32], + pub block_hash: [u8; 32], + pub metadata_hash: Option, + pub app_id: Option, +} + +impl ExtrinsicPayload { + /// Encodes the payload using the Substrate transaction format. + /// The format is: version + era + nonce + tip + call + params + pub fn to_bytes(&self) -> Vec { + let mut encoded = Vec::new(); + encoded.extend(self.call.clone()); + encoded.extend(&self.era.clone()); + encoded.extend(Compact(self.nonce).encode()); + encoded.extend(Compact(self.tip).encode()); + + // Use the app_id if it is set for AVAIL transactions, otherwise use the mode + if let Some(app_id) = self.app_id { + encoded.extend(Compact(app_id).encode()); + } else if let Some(mode) = self.mode { + encoded.extend(mode.encode()); + } + + encoded.extend(&self.spec_version.encode()); + encoded.extend(&self.transaction_version.encode()); + encoded.extend(&self.genesis_hash); + encoded.extend(&self.block_hash); + + // Use the metadata_hash if it is not set for AVAIL transactions + if self.app_id.is_none() { + if let Some(metadata_hash) = self.metadata_hash { + encoded.push(metadata_hash); + } + } + + encoded + } + + /// Encodes the payload with a signature using the Substrate transaction format. + /// The format is: length + (version + signature + era + nonce + tip + call + params) + pub fn encode_with_signature(&self, public_key: &[u8; 32], signature: &[u8]) -> Vec { + let mut encoded = Vec::new(); + + encoded.push(SIGNED_FLAG | TRANSACTION_VERSION); + + encoded.push(PUBLIC_KEY_TYPE); + encoded.extend_from_slice(public_key); + + encoded.push(SIGNATURE_TYPE); + + encoded.extend_from_slice(signature); + + encoded.extend_from_slice(&self.era); + encoded.extend_from_slice(&Compact(self.nonce).encode()); + encoded.extend_from_slice(&Compact(self.tip).encode()); + + // Use the app_id if it is set for AVAIL transactions, otherwise use the mode + if let Some(app_id) = self.app_id { + encoded.extend_from_slice(Compact(app_id).encode().as_slice()); + } else if let Some(mode) = self.mode { + encoded.push(mode); + } + + encoded.extend_from_slice(&self.call); + + let length = Compact(encoded.len() as u32).encode(); + let mut complete_encoded = Vec::with_capacity(length.len() + encoded.len()); + complete_encoded.extend_from_slice(&length); + complete_encoded.extend_from_slice(&encoded); + + complete_encoded + } +} diff --git a/packages/kos-codec/src/chains/trx/mod.rs b/packages/kos-codec/src/chains/trx/mod.rs new file mode 100644 index 00000000..79f2476b --- /dev/null +++ b/packages/kos-codec/src/chains/trx/mod.rs @@ -0,0 +1,96 @@ +use crate::protos::generated::trx::protocol; +use kos::chains::{ChainError, Transaction}; +use kos::crypto::hash::sha256_digest; +use prost::Message; + +pub fn encode_for_sign(mut transaction: Transaction) -> Result { + let tron_tx = decode_transaction(transaction.raw_data.clone())?; + + let raw_data_clone = tron_tx + .raw_data + .clone() + .ok_or(ChainError::ProtoDecodeError)?; + let mut tx_raw = Vec::new(); + raw_data_clone.encode(&mut tx_raw)?; + + transaction.tx_hash = sha256_digest(&tx_raw[..]).to_vec(); + + Ok(transaction) +} + +pub fn encode_for_broadcast(mut transaction: Transaction) -> Result { + let mut tron_tx = decode_transaction(transaction.raw_data.clone())?; + tron_tx.signature.push(transaction.signature.clone()); + + let mut tx_data = Vec::new(); + tron_tx.encode(&mut tx_data)?; + + transaction.raw_data = tx_data; + + Ok(transaction) +} + +pub fn decode_transaction(raw_tx: Vec) -> Result { + let tx = protocol::Transaction::decode(raw_tx.as_slice()); + if let Ok(t) = tx { + return Ok(t); + } + + let raw_tx = protocol::transaction::Raw::decode(raw_tx.as_slice())?; + let tx = protocol::Transaction { + raw_data: Some(raw_tx), + signature: vec![], + ret: vec![], + }; + + Ok(tx) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_encode_for_sign() { + let raw_tx = hex::decode( + "0a02487c22080608af18f6ec6c8340d8f8fae2e0315a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a1541e825d52582eec346c839b4875376117904a76cbc12154120ab1300cf70c048e4cf5d5b1b33f59653ed6626180a708fb1f7e2e031" + ).unwrap(); + let tx = Transaction { + raw_data: raw_tx.clone(), + tx_hash: vec![], + signature: vec![], + options: None, + }; + + let result = encode_for_sign(tx).unwrap(); + + assert_eq!( + hex::encode(result.tx_hash), + "96a09fd664f1a7abbbe8bca604ea40b80291119fed5283c71ba94882d5b3c8a5" + ); + } + + #[test] + fn test_encode_for_broadcast() { + let raw_tx = hex::decode( + "0a02487c22080608af18f6ec6c8340d8f8fae2e0315a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a1541e825d52582eec346c839b4875376117904a76cbc12154120ab1300cf70c048e4cf5d5b1b33f59653ed6626180a708fb1f7e2e031" + ).unwrap(); + let tx = Transaction { + raw_data: raw_tx.clone(), + tx_hash: vec![], + signature: vec![ + 2, 0, 0, 0, 72, 0, 0, 0, 48, 69, 34, 1, 9, 150, 43, 40, 55, 79, 163, 209, 160, 3, + 67, 48, 254, 119, 69, 171, 2, 219, 7, 205, 55, 100, 73, 230, 77, 110, 109, + ], + options: None, + }; + + let result = encode_for_broadcast(tx).unwrap(); + + assert_eq!(hex::encode(result.raw_data), "0a83010a02487c22080608af18f6ec6c8340d8f8fae2e0315a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a1541e825d52582eec346c839b4875376117904a76cbc12154120ab1300cf70c048e4cf5d5b1b33f59653ed6626180a708fb1f7e2e031122702000000480000003045220109962b28374fa3d1a0034330fe7745ab02db07cd376449e64d6e6d"); + assert_eq!( + hex::encode(result.signature), + "02000000480000003045220109962b28374fa3d1a0034330fe7745ab02db07cd376449e64d6e6d" + ); + } +} diff --git a/packages/kos-codec/src/lib.rs b/packages/kos-codec/src/lib.rs index 59fc9812..679f77f1 100644 --- a/packages/kos-codec/src/lib.rs +++ b/packages/kos-codec/src/lib.rs @@ -1,9 +1,10 @@ mod chains; +mod protos; -use crate::chains::{ada, apt, atom, bch, xrp}; +use crate::chains::{ada, apt, atom, bch, btc, eth, icp, klv, sol, substrate, trx, xrp}; use kos::chains::{get_chain_by_base_id, ChainError, ChainType, Transaction}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct KosCodedAccount { pub chain_id: u32, pub address: String, @@ -24,6 +25,13 @@ pub fn encode_for_signing( ChainType::ADA => ada::encode_for_sign(transaction)?, ChainType::ATOM => atom::encode_for_sign(transaction)?, ChainType::BCH => bch::encode_for_sign(transaction)?, + ChainType::BTC => btc::encode_for_sign(transaction)?, + ChainType::KLV => klv::encode_for_sign(transaction)?, + ChainType::TRX => trx::encode_for_sign(transaction)?, + ChainType::SOL => sol::encode_for_sign(transaction)?, + ChainType::ETH => eth::encode_for_sign(transaction)?, + ChainType::SUBSTRATE => substrate::encode_for_sign(transaction, account)?, + ChainType::ICP => icp::encode_for_sign(transaction)?, _ => transaction, }) } @@ -43,6 +51,13 @@ pub fn encode_for_broadcast( ChainType::ATOM => atom::encode_for_broadcast(transaction)?, ChainType::APT => apt::encode_for_broadcast(transaction, account)?, ChainType::BCH => bch::encode_for_broadcast(transaction, account.public_key)?, + ChainType::BTC => btc::encode_for_broadcast(transaction, account.public_key)?, + ChainType::KLV => klv::encode_for_broadcast(transaction)?, + ChainType::TRX => trx::encode_for_broadcast(transaction)?, + ChainType::SOL => sol::encode_for_broadcast(transaction)?, + ChainType::ETH => eth::encode_for_broadcast(transaction)?, + ChainType::SUBSTRATE => substrate::encode_for_broadcast(transaction, account)?, + ChainType::ICP => icp::encode_for_broadcast(transaction)?, _ => transaction, }) } diff --git a/packages/kos/src/protos/generated/klv/google.protobuf.rs b/packages/kos-codec/src/protos/generated/klv/google.protobuf.rs similarity index 100% rename from packages/kos/src/protos/generated/klv/google.protobuf.rs rename to packages/kos-codec/src/protos/generated/klv/google.protobuf.rs diff --git a/packages/kos/src/protos/generated/klv/mod.rs b/packages/kos-codec/src/protos/generated/klv/mod.rs similarity index 100% rename from packages/kos/src/protos/generated/klv/mod.rs rename to packages/kos-codec/src/protos/generated/klv/mod.rs diff --git a/packages/kos/src/protos/generated/klv/proto.rs b/packages/kos-codec/src/protos/generated/klv/proto.rs similarity index 100% rename from packages/kos/src/protos/generated/klv/proto.rs rename to packages/kos-codec/src/protos/generated/klv/proto.rs diff --git a/packages/kos/src/protos/generated/mod.rs b/packages/kos-codec/src/protos/generated/mod.rs similarity index 100% rename from packages/kos/src/protos/generated/mod.rs rename to packages/kos-codec/src/protos/generated/mod.rs diff --git a/packages/kos/src/protos/generated/trx/google.protobuf.rs b/packages/kos-codec/src/protos/generated/trx/google.protobuf.rs similarity index 100% rename from packages/kos/src/protos/generated/trx/google.protobuf.rs rename to packages/kos-codec/src/protos/generated/trx/google.protobuf.rs diff --git a/packages/kos/src/protos/generated/trx/mod.rs b/packages/kos-codec/src/protos/generated/trx/mod.rs similarity index 100% rename from packages/kos/src/protos/generated/trx/mod.rs rename to packages/kos-codec/src/protos/generated/trx/mod.rs diff --git a/packages/kos/src/protos/generated/trx/protocol.rs b/packages/kos-codec/src/protos/generated/trx/protocol.rs similarity index 100% rename from packages/kos/src/protos/generated/trx/protocol.rs rename to packages/kos-codec/src/protos/generated/trx/protocol.rs diff --git a/packages/kos/src/protos/klever/contracts.proto b/packages/kos-codec/src/protos/klever/contracts.proto similarity index 100% rename from packages/kos/src/protos/klever/contracts.proto rename to packages/kos-codec/src/protos/klever/contracts.proto diff --git a/packages/kos/src/protos/klever/transaction.proto b/packages/kos-codec/src/protos/klever/transaction.proto similarity index 100% rename from packages/kos/src/protos/klever/transaction.proto rename to packages/kos-codec/src/protos/klever/transaction.proto diff --git a/packages/kos/src/protos/klever/userAccountData.proto b/packages/kos-codec/src/protos/klever/userAccountData.proto similarity index 100% rename from packages/kos/src/protos/klever/userAccountData.proto rename to packages/kos-codec/src/protos/klever/userAccountData.proto diff --git a/packages/kos/src/protos/mod.rs b/packages/kos-codec/src/protos/mod.rs similarity index 92% rename from packages/kos/src/protos/mod.rs rename to packages/kos-codec/src/protos/mod.rs index 6e4a9cc4..7dd90460 100644 --- a/packages/kos/src/protos/mod.rs +++ b/packages/kos-codec/src/protos/mod.rs @@ -1,7 +1,6 @@ pub mod generated; -use crate::crypto::base64::{simple_base64_decode, simple_base64_encode}; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; + +use kos::crypto::base64::{simple_base64_decode, simple_base64_encode}; use tiny_json_rs::mapper; use tiny_json_rs::mapper::Value; use tiny_json_rs::serializer::DecodeError; diff --git a/packages/kos/src/protos/tron b/packages/kos-codec/src/protos/tron similarity index 100% rename from packages/kos/src/protos/tron rename to packages/kos-codec/src/protos/tron diff --git a/packages/kos-hardware/src/lib.rs b/packages/kos-hardware/src/lib.rs index f8ef8e37..2855a966 100644 --- a/packages/kos-hardware/src/lib.rs +++ b/packages/kos-hardware/src/lib.rs @@ -1,17 +1,23 @@ #![no_std] #![allow(clippy::to_string_in_format_args)] +extern crate alloc; use kos::chains; mod models; -extern crate alloc; - +use crate::alloc::borrow::ToOwned; use crate::models::{CBuffer, CNodeStruct, CTransaction, CTxInfo, RequestChainParams}; use alloc::format; use alloc::string::{String, ToString}; #[allow(unused_imports)] use core::alloc::GlobalAlloc; +use kos::chains::TxType; +use tiny_json_rs::lexer::StringType; + +use tiny_json_rs::mapper; +use tiny_json_rs::serializer; +use tiny_json_rs::serializer::Token; #[allow(dead_code)] struct FreeRtosAllocator; @@ -330,7 +336,49 @@ pub extern "C" fn rs_mnemonic_to_seed( #[no_mangle] pub extern "C" fn rs_tx_info_to_json(info: &mut CTxInfo, result: &mut CBuffer) -> bool { let tx_info = info.to_tx_info(); - let json = tiny_json_rs::encode(tx_info); + + enum TransactionType { + Unknown, + Transfer, + TriggerContract, + } + + #[derive(tiny_json_rs::Serialize)] + struct TransactionDetails { + pub sender: String, + pub receiver: String, + pub value: f64, + pub tx_type: TransactionType, + } + + let transaction_details = TransactionDetails { + sender: tx_info.sender, + receiver: tx_info.receiver, + value: tx_info.value, + tx_type: match tx_info.tx_type { + TxType::Unknown => TransactionType::Unknown, + TxType::Transfer => TransactionType::Transfer, + TxType::TriggerContract => TransactionType::TriggerContract, + }, + }; + + impl serializer::Serialize for TransactionType { + fn serialize(&self) -> mapper::Value { + let str = match self { + TransactionType::Unknown => "Unknown", + TransactionType::Transfer => "Transfer", + TransactionType::TriggerContract => "TriggerContract", + }; + let token = Token { + token_type: tiny_json_rs::lexer::TokenType::String(StringType::SimpleString), + literal: str.to_string(), + }; + + mapper::Value::Token(token) + } + } + + let json = tiny_json_rs::encode(transaction_details); result.write(json.as_ptr(), json.len() as u32); true diff --git a/packages/kos-mobile/src/lib.rs b/packages/kos-mobile/src/lib.rs index 4e3d192c..04d6d11e 100644 --- a/packages/kos-mobile/src/lib.rs +++ b/packages/kos-mobile/src/lib.rs @@ -154,6 +154,13 @@ fn validate_mnemonic(mnemonic: String) -> bool { kos::crypto::mnemonic::validate_mnemonic(mnemonic.as_str()).is_ok() } +#[uniffi::export] +fn get_path_by_chain(chain_id: u32, index: u32, use_legacy_path: bool) -> Result { + let chain = get_chain_by(chain_id)?; + let path = chain.get_path(index, use_legacy_path); + Ok(path) +} + #[uniffi::export] fn generate_wallet_from_mnemonic( mnemonic: String, @@ -304,10 +311,11 @@ fn sign_transaction( let pk = hex::decode(account.private_key.clone())?; let signed_transaction = chain.sign_tx(pk, encoded)?; - let signature = signed_transaction.signature.clone(); let encoded_to_broadcast = encode_for_broadcast(kos_codec_acc, signed_transaction)?; + let signature = encoded_to_broadcast.signature.clone(); + Ok(KOSTransaction { chain_id: account.chain_id, raw: hex::encode(encoded_to_broadcast.raw_data), @@ -522,7 +530,7 @@ mod tests { } #[test] - fn should_sign_raw_transaction() { + fn should_sign_raw_transaction_klv() { let chain_id = 38; let raw = hex::encode("{\"RawData\":{\"BandwidthFee\":1000000,\"ChainID\":\"MTAwNDIw\",\"Contract\":[{\"Parameter\":{\"type_url\":\"type.googleapis.com/proto.TransferContract\",\"value\":\"CiAysyg0Aj8xj/rr5XGU6iJ+ATI29mnRHS0W0BrC1vz0CBgK\"}}],\"KAppFee\":500000,\"Nonce\":39,\"Sender\":\"5BsyOlcf2VXgnNQWYP9EZcP0RpPIfy+upKD8QIcnyOo=\",\"Version\":1}}".as_bytes()); @@ -551,6 +559,115 @@ mod tests { "The signature doesn't match" ); } + #[test] + fn should_sign_raw_transaction_trx() { + let chain_id = 1; + + let raw = + "0a02487c22080608af18f6ec6c8340d8f8fae2e0315a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a1541e825d52582eec346c839b4875376117904a76cbc12154120ab1300cf70c048e4cf5d5b1b33f59653ed6626180a708fb1f7e2e031"; + + let account = generate_wallet_from_mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), + chain_id, + 0, + false, + ) + .unwrap(); + + let transaction = sign_transaction(account, raw.to_string(), None).unwrap(); + + assert_eq!(transaction.chain_id, chain_id, "The chain_id doesn't match"); + assert_eq!( + transaction.sender, "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH", + "The sender doesn't match" + ); + assert_eq!( + transaction.raw, "0a83010a02487c22080608af18f6ec6c8340d8f8fae2e0315a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a1541e825d52582eec346c839b4875376117904a76cbc12154120ab1300cf70c048e4cf5d5b1b33f59653ed6626180a708fb1f7e2e0311241e8469947140bdaff5cce4000e60a3bd95ca3de551870a450ce51ab41acfefe8b009e7ca1caaad63efdae94332f6282ef8766471236849511e70d7b1c22c15f7b01", + "The raw doesn't match" + ); + assert_eq!( + transaction.signature, "e8469947140bdaff5cce4000e60a3bd95ca3de551870a450ce51ab41acfefe8b009e7ca1caaad63efdae94332f6282ef8766471236849511e70d7b1c22c15f7b01", + "The signature doesn't match" + ); + } + #[test] + fn should_sign_raw_transaction_sol() { + let chain_id = 40; + + let raw = + "00010000030101010101010101010101010101010101010101010101010101010101010101020202020202020202020202020202020202020202020202020202020202020203030303030303030303030303030303030303030303030303030303030303032a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a01020200010c020000006400000000000000"; + + let account = generate_wallet_from_mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), + chain_id, + 0, + false, + ) + .unwrap(); + + let transaction = sign_transaction(account, raw.to_string(), None).unwrap(); + + assert_eq!( + transaction.raw, "01ed844199837f89a97752816386224313026513146985748655927567a596ad04f66f504273eae87b4ec6b0166641f35f27d7b412166b2cc23d2992102b985203010000030101010101010101010101010101010101010101010101010101010101010101020202020202020202020202020202020202020202020202020202020202020203030303030303030303030303030303030303030303030303030303030303032a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a01020200010c020000006400000000000000", + "The raw doesn't match" + ); + assert_eq!( + transaction.signature, "ed844199837f89a97752816386224313026513146985748655927567a596ad04f66f504273eae87b4ec6b0166641f35f27d7b412166b2cc23d2992102b985203", + "The signature doesn't match" + ); + } + #[test] + fn should_sign_raw_legacy_transaction_sol() { + let chain_id = 40; + + let raw = + "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010002049a3c6870aeb9068f2bf9eddc8fb19b3d579da42c31f83099279ed3c377cc3747b97530182dceb9d42c01c0581af062c94ecae225cfc500fdc695b85f1063a27400000000000000000000000000000000000000000000000000000000000000000306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a40000000a0daf9b9fa585f46e77f3ca63a84432074a910f08ee3b69c4316392720a457190303000502490200000300090380969800000000000202000114020000000100000000000000b2607248be872c18"; + + let account = generate_wallet_from_mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), + chain_id, + 0, + false, + ) + .unwrap(); + + let transaction = sign_transaction(account, raw.to_string(), None).unwrap(); + + assert_eq!( + transaction.raw, "01b079c666c9ff53bb26d7606d10131ebbc8d398dac9fd1285d5138bbdd521758d7a6b6bdb2876730637704eb1511f3f7d842343b9e406bb3e3583d6588949a904010002049a3c6870aeb9068f2bf9eddc8fb19b3d579da42c31f83099279ed3c377cc3747b97530182dceb9d42c01c0581af062c94ecae225cfc500fdc695b85f1063a27400000000000000000000000000000000000000000000000000000000000000000306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a40000000a0daf9b9fa585f46e77f3ca63a84432074a910f08ee3b69c4316392720a457190303000502490200000300090380969800000000000202000114020000000100000000000000b2607248be872c18", + "The raw doesn't match" + ); + assert_eq!( + transaction.signature, "b079c666c9ff53bb26d7606d10131ebbc8d398dac9fd1285d5138bbdd521758d7a6b6bdb2876730637704eb1511f3f7d842343b9e406bb3e3583d6588949a904", + "The signature doesn't match" + ); + } + #[test] + fn should_sign_raw_v0_transaction_sol() { + let chain_id = 40; + + let raw = + "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800100060a9a3c6870aeb9068f2bf9eddc8fb19b3d579da42c31f83099279ed3c377cc374758ef677fb5635e6473724b70e16b640554034ea47a1c7b3fcd88853c415d325476b8050abc2986a13e443af9bf4ea4d310daf4ce761c12c5ac5622ae757c36d2b19942026d00b891714c2544c4f6919b7c4116ef7246443c88b215ee7ddf6eaf0000000000000000000000000000000000000000000000000000000000000000ac1f83fdb9ce550de95d558cdc795461ccf4374ac688ec13a98400220a78da060306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a40000000b43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e80479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138f06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a985e5e847a818aa8ed7e1a03d4b1dbf41ca5fe93a7317a75d56e8fbef5b3979640506000502e6be0100060009034491060000000000080503001309040993f17b64f484ae76ff08180900020308130107080f110b0002030e0a0d0c091212100523e517cb977ae3ad2a0100000019640001f82e010000000000c1ad0900000000002b000509030300000109010fe5dfa171f7e49e10a3d6a91b55bb5714a643b5e94e1e5af2fe8b34d5be4fb205e2e1e3e8c905e7e4e0e545"; + + let account = generate_wallet_from_mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), + chain_id, + 0, + false, + ) + .unwrap(); + + let transaction = sign_transaction(account, raw.to_string(), None).unwrap(); + + assert_eq!( + transaction.raw, "0140098643a37209b2e0984c2f55872ccf150c44a1100a16a985b1bc04b13c31f9d9d1b070229241df5aaa21af22e0e4f88b6371106766fd95096b67f1066f8701800100060a9a3c6870aeb9068f2bf9eddc8fb19b3d579da42c31f83099279ed3c377cc374758ef677fb5635e6473724b70e16b640554034ea47a1c7b3fcd88853c415d325476b8050abc2986a13e443af9bf4ea4d310daf4ce761c12c5ac5622ae757c36d2b19942026d00b891714c2544c4f6919b7c4116ef7246443c88b215ee7ddf6eaf0000000000000000000000000000000000000000000000000000000000000000ac1f83fdb9ce550de95d558cdc795461ccf4374ac688ec13a98400220a78da060306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a40000000b43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e80479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138f06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a985e5e847a818aa8ed7e1a03d4b1dbf41ca5fe93a7317a75d56e8fbef5b3979640506000502e6be0100060009034491060000000000080503001309040993f17b64f484ae76ff08180900020308130107080f110b0002030e0a0d0c091212100523e517cb977ae3ad2a0100000019640001f82e010000000000c1ad0900000000002b000509030300000109010fe5dfa171f7e49e10a3d6a91b55bb5714a643b5e94e1e5af2fe8b34d5be4fb205e2e1e3e8c905e7e4e0e545", + "The raw doesn't match" + ); + assert_eq!( + transaction.signature, "40098643a37209b2e0984c2f55872ccf150c44a1100a16a985b1bc04b13c31f9d9d1b070229241df5aaa21af22e0e4f88b6371106766fd95096b67f1066f8701", + "The signature doesn't match" + ); + } #[test] fn should_sign_raw_transaction_cosmos() { @@ -604,6 +721,146 @@ mod tests { assert_eq!(transaction.raw, "0100000002afa8838dbaa03cd3e4fee38bdcb6a428965559ae941dca5a8f91999cfd6d8b0d010000006b48304502210099626d28374fa3d1a0034330fee7745ab02db07cd37649e6d3ffbe046ff92e9402203793bee2372ab59a05b45188c2bace3b48e73209a01e4d5d862925971632c80a412102bbe7dbcdf8b2261530a867df7180b17a90b482f74f2736b8a30d3f756e42e217ffffffffdb6d60d4a93a95738e72f641bcdd166c94f6e1f439dfe695e40583997284463c010000006a4730440220447084aae4c6800db7c86b8bc8da675e464991a035b2b4010cde48b64a1013a10220582acfb5265c22eae9c2880e07ae66fc86cbef2e97a2ca1bc513535ba322360d412102bbe7dbcdf8b2261530a867df7180b17a90b482f74f2736b8a30d3f756e42e217ffffffff0240420f00000000001976a91434bf902df5d66f0e9b89d0f83fbcad638ad19ae988acea970700000000001976a9145bb0ba5ba58cdab459f27f2d29f40e1dd5db238188ac00000000", "The raw doesn't match"); } + #[test] + fn should_sign_raw_transaction_btc() { + let chain_id = kos::chains::btc::ID; + + let account = generate_wallet_from_mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), + chain_id, + 0, + false, + ) + .unwrap(); + + let transaction = sign_transaction( + account, + "0100000002badfa0606bc6a1738d8ddf951b1ebf9e87779934a5774b836668efb5a6d643970000000000fffffffffe60fbeb66791b10c765a207c900a08b2a9bd7ef21e1dd6e5b2ef1e9d686e5230000000000ffffffff028813000000000000160014e4132ab9175345e24b344f50e6d6764a651a89e6c21f000000000000160014546d5f8e86641e4d1eec5b9155a540d953245e4a00000000".to_string(), + Some(TransactionChainOptions::Btc { + prev_scripts: vec![ + hex::decode("0014546d5f8e86641e4d1eec5b9155a540d953245e4a").unwrap(), + hex::decode("0014546d5f8e86641e4d1eec5b9155a540d953245e4a").unwrap(), + ], + input_amounts: vec![5000, 10000], + }), + ) + .unwrap(); + + assert_eq!(transaction.raw, "01000000000102badfa0606bc6a1738d8ddf951b1ebf9e87779934a5774b836668efb5a6d643970000000000fffffffffe60fbeb66791b10c765a207c900a08b2a9bd7ef21e1dd6e5b2ef1e9d686e5230000000000ffffffff028813000000000000160014e4132ab9175345e24b344f50e6d6764a651a89e6c21f000000000000160014546d5f8e86641e4d1eec5b9155a540d953245e4a02483045022100ca1df8381e56e2ac2228e040cc2ff1c1079928222365f5c62cd6c18f398a6f55022029dca1177ab6edcfa03a25c7df32e1644c5d1fe496c6c7995a715373b56a591901210330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c024830450221009496122a56551a0dab4fa8562474c943c79158f7592a845abd7b60ddf34c10c902205021b73e27a44b0c365fbd015133a4bb6dce79dd09705096de1c7b31a1f9b8a701210330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c00000000", "The raw doesn't match"); + } + #[test] + fn should_sign_raw_transaction_dash() { + let chain_id = 11; + + let account = generate_wallet_from_mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), + chain_id, + 0, + false, + ) + .unwrap(); + + let transaction = sign_transaction( + account, + "0100000001c2c12c2f80249f568cf90ffd87d47afbbed81e803d2a7076c554e81b73253ab50000000000ffffffff02e8030000000000001976a914a3d92f1bab64bb8154ed118cd27fb5081344ca8488ac04580f00000000001976a914be4232b46086c1d46d12c65eacbd807e87b92a5488ac00000000".to_string(), + Some(TransactionChainOptions::Btc { + prev_scripts: vec![ + hex::decode("76a914be4232b46086c1d46d12c65eacbd807e87b92a5488ac").unwrap(), + ], + input_amounts: vec![1013578], + }), + ) + .unwrap(); + + assert_eq!(transaction.raw, "0100000001c2c12c2f80249f568cf90ffd87d47afbbed81e803d2a7076c554e81b73253ab5000000006a4730440220423d61c364084d0c24f155519d4991549b1090bdd65ac6c74ebc6f3917d5dff6022056c3af1de9b4e33369dcd134591a80554ce5a108c300bc0cc2ed4c11d0a6861c0121026fa9a6f213b6ba86447965f6b4821264aaadd7521f049f00db9c43a770ea7405ffffffff02e8030000000000001976a914a3d92f1bab64bb8154ed118cd27fb5081344ca8488ac04580f00000000001976a914be4232b46086c1d46d12c65eacbd807e87b92a5488ac00000000", "The raw doesn't match"); + } + + #[test] + fn should_sign_raw_transaction_eth() { + let chain_id = 3; + + let account = generate_wallet_from_mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), + chain_id, + 0, + false, + ) + .unwrap(); + + let transaction = sign_transaction( + account, + "b87602f8730182014f84147b7eeb85084ec9f83f8301450994dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb0000000000000000000000004cbeee256240c92a9ad920ea6f4d7df6466d2cdc000000000000000000000000000000000000000000000000000000000000000ac0808080".to_string(), + Some(TransactionChainOptions::Evm { + chain_id:1 + }), + ) + .unwrap(); + + assert_eq!(transaction.raw, "02f8b30182014f84147b7eeb85084ec9f83f8301450994dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb0000000000000000000000004cbeee256240c92a9ad920ea6f4d7df6466d2cdc000000000000000000000000000000000000000000000000000000000000000ac001a0ac17a21525645e7bdf653b2e46b4fb7b33668b0cb42ce38bf8fbb752e527fb63a0e56f5ff3e3eb15441eeaf144237204b0435ed31d0e009153512074fa56b2cc62", "The raw doesn't match"); + } + #[test] + fn should_sign_raw_transaction_dot() { + let chain_id = 21; + + let account = generate_wallet_from_mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), + chain_id, + 0, + false, + ) + .unwrap(); + + let options = TransactionChainOptions::Substrate { + call: hex::decode( + "0503000c2441b8cedbfc7a2edc0968b9a535819969d3e9e0998680babb5827287fc07004", + ) + .unwrap(), + era: hex::decode("d501").unwrap(), + nonce: 27, + tip: 0, + block_hash: hex::decode( + "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + ) + .unwrap(), + genesis_hash: hex::decode( + "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + ) + .unwrap(), + spec_version: 1003004, + transaction_version: 26, + app_id: None, + }; + + let transaction = sign_transaction( + account, + "0503000c2441b8cedbfc7a2edc0968b9a535819969d3e9e0998680babb5827287fc07004d5016c0000fc4d0f001a00000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c347ee1c48ed396721e74df1c81e95b2aab3e13763e8a2dec7ed5a8b94af4c808d00".to_string(), + Some(options), + ) + .unwrap(); + + assert_eq!(transaction.raw.len(), 284, "The raw length doesn't match"); + } + #[test] + fn should_sign_raw_transaction_icp() { + let chain_id = 31; + + let account = generate_wallet_from_mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), + chain_id, + 0, + false, + ) + .unwrap(); + + let transaction = sign_transaction( + account, + "35623232333036313336333933363333333236343337333233363335333733313337333533363335333733333337333433353332333133373635333533363336333433393636363633323331333233383333333636363330363133323332333633323336333736363333333036333631333033303635333836353334363633323635333833333334363236313336363333303634333236343338333233343332333733323336333933303337333033303232326332323330363133363339333633333332363433373332333633353337333133373335333633353337333333373334333136323331333433303632363236343337333633353337333136333330363133333334333333333331333333313338333533343333333736323635333836323338363236333334363433353635333233383339333833303331363136353631333133373635363133303336363136363332333036343331363533393337333532323564".to_string(), + None, + ) + .unwrap(); + + assert_eq!(transaction.signature, "5b226366623365373264373431353231613830336136613337363938363434313365656639353030646662356662343838643638623834303636663836343337383561363964613833653265633463393336653834303832373261643936643164343631643466393161323664643966623433643231663931333061373562393036222c223937636130633265656635363733656530353238623361666134363863666432626433333834623164643938643365346339313731383535626564386239313563386239373161623861383432623566623866633738666462376361383139373533663335353232396431666330643537633337303965303631356330353034225d", "The raw length doesn't match"); + } #[test] fn should_sign_transaction_with_options() { @@ -663,4 +920,16 @@ mod tests { "The supported chains should not be empty" ); } + + #[test] + fn should_get_path_by_chain() { + let path = get_path_by_chain(38, 0, false).unwrap(); + assert_eq!(path, "m/44'/690'/0'/0'/0'"); + + let path = get_path_by_chain(27, 0, false).unwrap(); + assert_eq!(path, ""); + + let path = get_path_by_chain(27, 1, false).unwrap(); + assert_eq!(path, "//0///"); + } } diff --git a/packages/kos-web/demo/src/components/cryptography-demo.ts b/packages/kos-web/demo/src/components/cryptography-demo.ts index 5667be9a..13fb7345 100644 --- a/packages/kos-web/demo/src/components/cryptography-demo.ts +++ b/packages/kos-web/demo/src/components/cryptography-demo.ts @@ -126,7 +126,16 @@ export class CryptographyDemo { break; } } catch (error) { - this.showError((error as Error).message); + if (error instanceof Error) { + this.showError(error.message); + return; + } + if (typeof error === "string") { + this.showError(error); + return; + } + + this.showError("An unknown error occurred"); } } diff --git a/packages/kos-web/demo/src/components/transaction-signer.ts b/packages/kos-web/demo/src/components/transaction-signer.ts index a2dfa958..8f24a11b 100644 --- a/packages/kos-web/demo/src/components/transaction-signer.ts +++ b/packages/kos-web/demo/src/components/transaction-signer.ts @@ -235,7 +235,16 @@ export class TransactionSigner { `Wallet successfully loaded! Address: ${this.wallet.getAddress()}` ); } catch (error) { - this.showError((error as Error).message); + if (error instanceof Error) { + this.showError(error.message); + return; + } + if (typeof error === "string") { + this.showError(error); + return; + } + + this.showError("An unknown error occurred"); } } @@ -267,7 +276,16 @@ export class TransactionSigner {

Signature (hex): ${hexSignature}

`); } catch (error) { - this.showError((error as Error).message); + if (error instanceof Error) { + this.showError(error.message); + return; + } + if (typeof error === "string") { + this.showError(error); + return; + } + + this.showError("An unknown error occurred"); } } @@ -323,13 +341,17 @@ export class TransactionSigner { // Free the transaction transaction.free(); - - // Free chain options - if (chainOptions) { - chainOptions.free(); - } } catch (error) { - this.showError((error as Error).message); + if (error instanceof Error) { + this.showError(error.message); + return; + } + if (typeof error === "string") { + this.showError(error); + return; + } + + this.showError("An unknown error occurred"); } } diff --git a/packages/kos-web/demo/src/components/wallet-generator.ts b/packages/kos-web/demo/src/components/wallet-generator.ts index c42e156a..8155cd54 100644 --- a/packages/kos-web/demo/src/components/wallet-generator.ts +++ b/packages/kos-web/demo/src/components/wallet-generator.ts @@ -253,7 +253,16 @@ export class WalletGenerator { // Free resources wallet.free(); } catch (error) { - this.showError((error as Error).message); + if (error instanceof Error) { + this.showError(error.message); + return; + } + if (typeof error === "string") { + this.showError(error); + return; + } + + this.showError("An unknown error occurred"); } } diff --git a/packages/kos-web/src/error.rs b/packages/kos-web/src/error.rs index 8e55b7a4..a15dacb9 100644 --- a/packages/kos-web/src/error.rs +++ b/packages/kos-web/src/error.rs @@ -11,7 +11,7 @@ pub enum Error { // JSON serialization error JSONSerde(String), // UnsupportedChain, - UnsupportedChain(&'static str), + UnsupportedChain(String), // InvalidMnemonic, InvalidMnemonic(&'static str), // InvalidPath, diff --git a/packages/kos-web/src/lib.rs b/packages/kos-web/src/lib.rs index e6c70809..22f966c2 100644 --- a/packages/kos-web/src/lib.rs +++ b/packages/kos-web/src/lib.rs @@ -71,6 +71,18 @@ pub fn is_chain_supported(chain: u32) -> bool { kos::chains::is_chain_supported(chain) } +#[wasm_bindgen(js_name = "getPathByChain")] +pub fn get_path_by_chain( + chain_id: u32, + index: u32, + use_legacy_path: bool, +) -> Result { + let chain = kos::chains::get_chain_by_id(chain_id) + .ok_or(Error::UnsupportedChain(format!("{}", chain_id)))?; + let path = chain.get_path(index, use_legacy_path); + Ok(path) +} + #[wasm_bindgen] #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/packages/kos/Cargo.toml b/packages/kos/Cargo.toml index 09d6c0c3..1505259c 100644 --- a/packages/kos/Cargo.toml +++ b/packages/kos/Cargo.toml @@ -7,7 +7,6 @@ license.workspace = true repository.workspace = true rust-version.workspace = true version.workspace = true -build = "build.rs" ## Only necessary if using Protobuf well-known types: [lib] @@ -19,12 +18,10 @@ hmac = { version = "0.12", default-features = false } sha2 = { version = "0.10", default-features = false } sha3 = { version = "0.10", default-features = false } prost = { version = "0.13.5", default-features = false, features = ["prost-derive"] } -prost-types = { version = "0.13.5", default-features = false } bech32 = { version = "0.9.1", default-features = false } blake2b-ref = { version = "0.3.1", default-features = false } rlp = { version = "0.5.0", default-features = false } hex = { version = "0.4.3", features = ["alloc"], default-features = false } -tiny-json-rs = "0.2.5" ripemd = { version = "0.1.3", default-features = false } schnorrkel = { version = "0.11.4", default-features = false } ed25519-dalek = { version = "2.1.0", features = ["hazmat"], default-features = false } @@ -32,8 +29,6 @@ pbkdf2 = { version = "0.12.2", features = ["sha2", "hmac", "password-hash"], def bip39-dict = { version = "0.1.0", features = ["english"], default-features = false } libsecp256k1 = { version = "0.7.1", features = ["hmac", "static-context"], default-features = false } rand_core = { version = "0.6.4", default-features = false } -parity-scale-codec = { version = "3.6.9", default-features = false, features = ["derive"] } -bitcoin = { version = "0.32.5", default-features = false } rand = { workspace = true, optional = true } coins-bip32 = { workspace = true, optional = true } @@ -48,15 +43,6 @@ cfb-mode = { version = "0.8", optional = true } cbc = { version = "0.1", features = ["block-padding", "std"], optional = true } serde = { version = "1.0.215", features = ["derive"], optional = true } -[build-dependencies] -prost-build = "0.13.5" -quote = "1.0.33" -prost-wkt-build = "0.6.0" -prost-wkt = { version = "0.6.0", default-features = false } -prost-wkt-types = { version = "0.6.0", default-features = false } -heck = "0.4.1" - - [features] default = ["not-ksafe"] not-ksafe = [ diff --git a/packages/kos/src/chains/btc/mod.rs b/packages/kos/src/chains/btc/mod.rs index 7040281a..d8af4ed5 100644 --- a/packages/kos/src/chains/btc/mod.rs +++ b/packages/kos/src/chains/btc/mod.rs @@ -1,5 +1,6 @@ use crate::chains::util::{private_key_from_vec, slice_from_vec}; -use crate::chains::{Chain, ChainError, ChainOptions, ChainType, Transaction, TxInfo}; +use crate::chains::Chain; +use crate::chains::{ChainError, ChainType, Transaction, TxInfo}; use crate::crypto::b58::b58enc; use crate::crypto::bip32; use crate::crypto::hash::{ripemd160_digest, sha256_digest}; @@ -8,7 +9,8 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; use alloc::{format, vec}; use bech32::{u5, Variant}; -use bitcoin::{ecdsa, secp256k1, sighash, Amount, Denomination, Psbt, ScriptBuf}; + +pub const ID: u32 = 2; const BITCOIN_MESSAGE_PREFIX: &str = "\x18Bitcoin Signed Message:\n"; @@ -31,7 +33,7 @@ impl Default for BTC { impl BTC { pub fn new() -> Self { - BTC::new_btc_based(2, "bc", 0, "BTC", "Bitcoin") + BTC::new_btc_based(ID, "bc", 0, "BTC", "Bitcoin") } pub fn new_btc_based(id: u32, addr_prefix: &str, bip44: u32, symbol: &str, name: &str) -> Self { @@ -172,114 +174,41 @@ impl Chain for BTC { Ok(self.get_addr_new(public_key)?) } - fn sign_tx(&self, private_key: Vec, tx: Transaction) -> Result { - let mut tx = tx; - - let options = tx.options.clone().ok_or(ChainError::MissingOptions)?; - - let (prev_scripts, input_amounts) = match options { - ChainOptions::BTC { - prev_scripts, - input_amounts, - } => (prev_scripts, input_amounts), - _ => { - return Err(ChainError::InvalidOptions); - } - }; - - let transaction: bitcoin::Transaction = - bitcoin::consensus::deserialize(tx.raw_data.as_ref()).unwrap(); - - let mut psbt = Psbt::from_unsigned_tx(transaction.clone()).unwrap(); - - let mut cache = sighash::SighashCache::new(transaction.clone()); - - let sk = secp256k1::SecretKey::from_slice(private_key.clone().as_slice()).unwrap(); - - let btc = BTC::new(); - let pk = btc.get_pbk(private_key)?; - - let public_key = bitcoin::PublicKey::from_slice(pk.as_slice()).unwrap(); - - let secp = bitcoin::secp256k1::Secp256k1::new(); - - let values = input_amounts - .iter() - .map(|x| Amount::from_str_in(&x.to_string(), Denomination::Satoshi).unwrap()) - .collect::>(); - - for inp_idx in 0..psbt.inputs.len() { - let utxo = bitcoin::TxOut { - value: values[inp_idx], - script_pubkey: prev_scripts[inp_idx].clone().into(), - }; - psbt.inputs[inp_idx].witness_utxo = Some(utxo); - - // Add non_witness_utxo - psbt.inputs[inp_idx].non_witness_utxo = Some(transaction.clone()); - } - - // sign inputs - for inp_idx in 0..psbt.inputs.len() { - // compute sighash - let (msg, sighash_ty) = psbt.sighash_ecdsa(inp_idx, &mut cache).unwrap(); - - // sign - let sig = ecdsa::Signature { - signature: secp.sign_ecdsa(&msg, &sk), - sighash_type: sighash_ty, - }; - - // insert signature - psbt.inputs[inp_idx].partial_sigs.insert(public_key, sig); + fn sign_tx( + &self, + private_key: Vec, + mut tx: Transaction, + ) -> Result { + if tx.tx_hash.is_empty() { + return Err(ChainError::InvalidTransaction( + "Transaction hash is empty".to_string(), + )); } + let pvk_bytes = private_key_from_vec(&private_key)?; - // finalize - for (inp_idx, _) in prev_scripts.iter().enumerate().take(psbt.inputs.len()) { - let script_pubkey_bytes = prev_scripts[inp_idx].clone(); - - let script_pubkey = bitcoin::Script::from_bytes(script_pubkey_bytes.as_slice()); + let mut signatures = Vec::new(); - // check if it is a legacy or segwit transaction - let is_legacy = script_pubkey.is_p2pkh(); - let is_segwit = script_pubkey.is_p2wpkh(); - - if let Some((pubkey, sig)) = psbt.inputs[inp_idx].partial_sigs.first_key_value() { - if is_legacy { - let script_sig_builder = bitcoin::Script::builder() - .push_slice(sig.serialize()) - .push_slice(pubkey.inner.serialize()); - - let script = script_sig_builder.as_script(); - - psbt.inputs[inp_idx].final_script_sig = Some(ScriptBuf::from(script)); - } else if is_segwit { - let mut script_witness = bitcoin::Witness::new(); - script_witness.push(sig.to_vec()); - script_witness.push(pubkey.to_bytes()); - - psbt.inputs[inp_idx].final_script_witness = Some(script_witness); - } else { - // unsupported script type - return Err(ChainError::UnsupportedScriptType); - } + // Each hash in the transaction is 32 bytes long + for hash in tx.tx_hash.chunks(32) { + if hash.len() != 32 { + return Err(ChainError::InvalidTransaction( + "Invalid hash length".to_string(), + )); } - } - let signed_tx = psbt - .extract_tx() - .map_err(|e| ChainError::InvalidTransaction(e.to_string()))?; + let mut hash_array = [0u8; 32]; + hash_array.copy_from_slice(hash); + + // Sign hash + let sig = Secp256K1::sign(&hash_array, &pvk_bytes)?; - tx.raw_data = bitcoin::consensus::encode::serialize(&signed_tx); + // The first 64 bytes are the signature, and the last byte is the recovery id + // We just need the first 64 bytes - let has_witness = signed_tx.input.iter().any(|x| !x.witness.is_empty()); - if has_witness { - tx.signature = bitcoin::consensus::encode::serialize(&signed_tx.compute_wtxid()); - } else { - tx.signature = bitcoin::consensus::encode::serialize(&signed_tx.compute_txid()); + signatures.extend_from_slice(&sig[0..64]); } - tx.tx_hash = bitcoin::consensus::encode::serialize(&signed_tx.compute_txid()); + tx.signature = signatures; Ok(tx) } @@ -329,7 +258,6 @@ impl Chain for BTC { #[cfg(test)] mod test { use super::*; - use crate::crypto::base64::simple_base64_decode; use alloc::string::ToString; #[test] @@ -443,63 +371,4 @@ mod test { assert_eq!(hex::encode(signature.clone()), "9d561a0ba6ea562e61606e7f3b6a92c889246eec2c05e86e3f465f43469ae9436d7e46accdcfaea848460e42c83c52238b6956c4bfb192e67023b6024e95bdcf01"); assert_eq!(signature.len(), 65); } - - #[test] - fn sign_transaction() { - let raw = hex::decode("0100000002badfa0606bc6a1738d8ddf951b1ebf9e87779934a5774b836668efb5a6d643970000000000fffffffffe60fbeb66791b10c765a207c900a08b2a9bd7ef21e1dd6e5b2ef1e9d686e5230000000000ffffffff028813000000000000160014e4132ab9175345e24b344f50e6d6764a651a89e6c21f000000000000160014546d5f8e86641e4d1eec5b9155a540d953245e4a00000000").unwrap(); - - let pvk = hex::decode("4604b4b710fe91f584fff084e1a9159fe4f8408fff380596a604948474ce4fa3") - .unwrap(); - let btc = BTC::new(); - let transaction = Transaction { - raw_data: raw, - signature: vec![], - tx_hash: vec![], - options: Option::from(ChainOptions::BTC { - prev_scripts: vec![ - hex::decode("0014546d5f8e86641e4d1eec5b9155a540d953245e4a").unwrap(), - hex::decode("0014546d5f8e86641e4d1eec5b9155a540d953245e4a").unwrap(), - ], - input_amounts: vec![5000, 10000], - }), - }; - - let signed_tx = btc.sign_tx(pvk, transaction).unwrap(); - - assert_eq!(signed_tx.signature.len(), 32); - assert_eq!(signed_tx.tx_hash.len(), 32); - assert_eq!(signed_tx.raw_data.len(), 372); - } - - #[test] - fn sign_transaction_legacy() { - let mnemonic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - - let dash = BTC::new_legacy_btc_based(11, 0x4C, 5, "DASH", "Dash"); - let path = dash.get_path(1, true); - - let seed = dash.mnemonic_to_seed(mnemonic, "".to_string()).unwrap(); - let pvk = dash.derive(seed, path).unwrap(); - - let raw = simple_base64_decode("AQAAAAHCwSwvgCSfVoz5D/2H1Hr7vtgegD0qcHbFVOgbcyU6tQAAAAAA/////wLoAwAAAAAAABl2qRSj2S8bq2S7gVTtEYzSf7UIE0TKhIisBFgPAAAAAAAZdqkUvkIytGCGwdRtEsZerL2Afoe5KlSIrAAAAAA=").unwrap(); - - let transaction = Transaction { - raw_data: raw, - signature: vec![], - tx_hash: vec![], - options: Option::from(ChainOptions::BTC { - prev_scripts: vec![ - simple_base64_decode("dqkUvkIytGCGwdRtEsZerL2Afoe5KlSIrA==").unwrap() - ], - input_amounts: vec![1013578], - }), - }; - - let signed_tx = dash.sign_tx(pvk, transaction).unwrap(); - - assert_eq!(signed_tx.signature.len(), 32); - assert_eq!(signed_tx.tx_hash.len(), 32); - assert_eq!(signed_tx.raw_data.len(), 225); - } } diff --git a/packages/kos/src/chains/eth/mod.rs b/packages/kos/src/chains/eth/mod.rs index cbf74abf..03d25064 100644 --- a/packages/kos/src/chains/eth/mod.rs +++ b/packages/kos/src/chains/eth/mod.rs @@ -1,8 +1,5 @@ -mod models; - -use crate::chains::eth::models::EthereumTransaction; use crate::chains::util::{private_key_from_vec, slice_from_vec}; -use crate::chains::{Chain, ChainError, ChainType, Transaction, TxInfo, TxType}; +use crate::chains::{Chain, ChainError, ChainType, Transaction, TxInfo}; use crate::crypto::hash::keccak256_digest; use crate::crypto::secp256k1::{Secp256K1, Secp256k1Trait}; use crate::crypto::{bip32, secp256k1}; @@ -122,30 +119,12 @@ impl Chain for ETH { private_key: Vec, mut tx: Transaction, ) -> Result { - let mut eth_tx = EthereumTransaction::decode(&tx.raw_data)?; - - //Ensure empty signature - eth_tx.signature = None; - if eth_tx.transaction_type == models::TransactionType::Legacy { - eth_tx.chain_id = Some(self.chaincode as u64); - } - - let new_rlp = eth_tx.encode()?; - let to_sign = keccak256_digest(&new_rlp[..]); - let signature = self.sign_raw(private_key, to_sign.to_vec())?; + let signature = self.sign_raw(private_key, tx.tx_hash.clone())?; if signature.len() != 65 { return Err(ChainError::InvalidSignature); } - - let _sig_hex = hex::encode(&signature[..]); - - let mut signature_bytes: [u8; 65] = [0; 65]; - signature_bytes.copy_from_slice(&signature[..]); - eth_tx.signature = Some(signature_bytes); - let signed_rlp = eth_tx.encode()?; - tx.raw_data = signed_rlp.clone(); - tx.tx_hash = Vec::from(keccak256_digest(&signed_rlp[..])); tx.signature = signature.to_vec(); + Ok(tx) } @@ -194,26 +173,8 @@ impl Chain for ETH { Ok(sig.to_vec()) } - fn get_tx_info(&self, raw_tx: Vec) -> Result { - let eth_tx = EthereumTransaction::decode(raw_tx.as_slice())?; - let mut tx_info = TxInfo { - sender: String::new(), - receiver: String::new(), - value: 0.0, - tx_type: TxType::Transfer, - }; - - if eth_tx.to.is_some() { - tx_info.tx_type = TxType::TriggerContract; - let addr_vec = eth_tx.to.unwrap_or([0; ETH_ADDR_SIZE].to_vec()); - let mut address_bytes: [u8; ETH_ADDR_SIZE] = [0; ETH_ADDR_SIZE]; - address_bytes.copy_from_slice(&addr_vec[..]); - tx_info.receiver = ETH::addr_bytes_to_string(address_bytes)?; - } - - tx_info.value = eth_tx.value.to_f64(self.get_decimals()); - - Ok(tx_info) + fn get_tx_info(&self, _raw_tx: Vec) -> Result { + Err(ChainError::NotSupported) } fn get_chain_type(&self) -> ChainType { @@ -225,7 +186,6 @@ impl Chain for ETH { mod test { use crate::chains::Chain; use alloc::string::ToString; - use alloc::vec::Vec; #[test] fn test_derive() { @@ -240,58 +200,6 @@ mod test { assert_eq!(addr, "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"); } - #[test] - fn test_sign_tx() { - let raw_tx = hex::decode( - "b302f101819e84ae7937b285035f6cccc58252089498de4c83810b87f0e2cd92d80c9fac28c4ded4818568c696991f80c0808080", - ) - .unwrap(); - let pvk = hex::decode("1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727") - .unwrap(); - let eth = super::ETH::new(); - - let tx = crate::chains::Transaction { - raw_data: raw_tx, - tx_hash: Vec::new(), - signature: Vec::new(), - options: None, - }; - - let _ = eth.sign_tx(pvk, tx).unwrap(); - } - - #[test] - fn test_sign_london_tx() { - let raw_tx = hex::decode("b87602f8730182014f84147b7eeb85084ec9f83f8301450994dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb0000000000000000000000004cbeee256240c92a9ad920ea6f4d7df6466d2cdc000000000000000000000000000000000000000000000000000000000000000ac0808080").unwrap(); - let pvk = hex::decode("1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727") - .unwrap(); - let eth = super::ETH::new_eth_based(3, 56, "ETH", "Ethereum"); - - let tx = crate::chains::Transaction { - raw_data: raw_tx, - tx_hash: Vec::new(), - signature: Vec::new(), - options: None, - }; - - let _ = eth.sign_tx(pvk, tx).unwrap(); - } - - #[test] - fn test_decode_tx() { - let raw_tx = hex::decode( - "ad02eb01038493a7d5d085068da15595825208944cbeee256240c92a9ad920ea6f4d7df6466d2cdc0a80c0808080", - ) - .unwrap(); - let eth = super::ETH::new(); - let tx_info = eth.get_tx_info(raw_tx).unwrap(); - assert_eq!( - tx_info.receiver, - "0x4cBeee256240c92A9ad920ea6f4d7Df6466D2Cdc" - ); - assert_eq!(tx_info.value, 4.523128485832664e57); - } - #[test] fn test_sign_typed_data() { let data = r#"{ diff --git a/packages/kos/src/chains/icp/mod.rs b/packages/kos/src/chains/icp/mod.rs index cce41f9b..d32c220c 100644 --- a/packages/kos/src/chains/icp/mod.rs +++ b/packages/kos/src/chains/icp/mod.rs @@ -1,4 +1,4 @@ -use crate::chains::util::private_key_from_vec; +use crate::chains::util::{byte_vectors_to_bytes, bytes_to_byte_vectors, private_key_from_vec}; use crate::chains::{Chain, ChainError, ChainType, Transaction, TxInfo}; use crate::crypto::bip32; use crate::crypto::ed25519::{Ed25519, Ed25519Trait}; @@ -6,7 +6,6 @@ use crate::crypto::hash::sha224_digest; use alloc::string::{String, ToString}; use alloc::vec::Vec; use alloc::{format, vec}; -use tiny_json_rs::serializer; const ASN1_ED25519_HEADER: [u8; 12] = [48u8, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0]; const ICP_TAIL: u8 = 2; @@ -82,37 +81,18 @@ impl Chain for ICP { private_key: Vec, mut tx: Transaction, ) -> Result { - let hex = hex::decode(tx.raw_data.clone()).map_err(|_| ChainError::DecodeRawTx)?; - let raw_data_str = String::from_utf8(hex).map_err(|_| ChainError::DecodeRawTx)?; - - let wrapped_data = format!("{{\"hashes\":{}}}", raw_data_str); - - #[derive(tiny_json_rs::Deserialize)] - struct HashContainer { - hashes: Vec, - } - - let container: HashContainer = - tiny_json_rs::decode(wrapped_data).map_err(|_| ChainError::DecodeHash)?; - - let icp_hashes = container.hashes; - + let icp_hashes = bytes_to_byte_vectors(tx.tx_hash.clone())?; let mut pvk_bytes = private_key_from_vec(&private_key)?; + let mut signatures = Vec::new(); - let mut signatures: Vec = Vec::new(); - - for hash_hex in icp_hashes { - let hash_bytes = hex::decode(&hash_hex).map_err(|_| ChainError::DecodeHash)?; - - let signature = Ed25519::sign(&pvk_bytes, &hash_bytes)?; - signatures.push(hex::encode(signature)); + for hash in icp_hashes { + let signature = Ed25519::sign(&pvk_bytes, &hash)?; + signatures.push(signature); } pvk_bytes.fill(0); - let signatures_json = tiny_json_rs::encode(signatures); - - tx.signature = signatures_json.into_bytes(); + tx.signature = byte_vectors_to_bytes(&signatures); if tx.tx_hash.is_empty() { tx.tx_hash = Vec::new(); @@ -147,7 +127,7 @@ impl Chain for ICP { } fn get_tx_info(&self, _raw_tx: Vec) -> Result { - todo!() + Err(ChainError::NotSupported) } fn get_chain_type(&self) -> ChainType { @@ -203,7 +183,6 @@ pub fn crc_calc_singletable(buffer: &[u8]) -> u32 { #[cfg(test)] mod test { use super::*; - use crate::crypto::base64::simple_base64_decode; #[test] fn test_icp_get_address() { let icp = ICP {}; @@ -221,29 +200,6 @@ mod test { ); } - #[test] - fn test_icp_sign_tx() { - let icp = ICP {}; - - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let seed = icp.mnemonic_to_seed(mnemonic, "".to_string()).unwrap(); - let path = icp.get_path(0, false); - let pvk = icp.derive(seed, path).unwrap(); - - let raw_data = simple_base64_decode("NWIyMjMwNjEzNjM5MzYzMzMyNjQzNzMyMzYzNTM3MzEzNzM1MzYzNTM3MzMzNzM0MzUzMjMxMzc2NTM1MzYzNjM0Mzk2NjY2MzIzMTMyMzgzMzM2NjYzMDYxMzIzMjM2MzIzNjM3NjYzMzMwNjM2MTMwMzA2NTM4NjUzNDY2MzI2NTM4MzMzNDYyNjEzNjYzMzA2NDMyNjQzODMyMzQzMjM3MzIzNjM5MzAzNzMwMzAyMjJjMjIzMDYxMzYzOTM2MzMzMjY0MzczMjM2MzUzNzMxMzczNTM2MzUzNzMzMzczNDMxNjIzMTM0MzA2MjYyNjQzNzM2MzUzNzMxNjMzMDYxMzMzNDMzMzMzMTMzMzEzODM1MzQzMzM3NjI2NTM4NjIzODYyNjMzNDY0MzU2NTMyMzgzOTM4MzAzMTYxNjU2MTMxMzc2NTYxMzAzNjYxNjYzMjMwNjQzMTY1MzkzNzM1MjI1ZA==").unwrap(); - - let tx = Transaction { - raw_data, - signature: vec![], - tx_hash: vec![], - options: None, - }; - - let signed_tx = icp.sign_tx(pvk, tx).unwrap(); - - assert_eq!(signed_tx.signature.len(), 263); - } - #[test] fn test_icp_sign_message() { let icp = ICP {}; diff --git a/packages/kos/src/chains/klv/mod.rs b/packages/kos/src/chains/klv/mod.rs index 307bb0c3..f1d5bb2e 100644 --- a/packages/kos/src/chains/klv/mod.rs +++ b/packages/kos/src/chains/klv/mod.rs @@ -1,19 +1,13 @@ -mod models; - -use crate::chains::{Chain, ChainError, ChainType, Transaction, TxInfo, TxType}; +use crate::chains::{Chain, ChainError, ChainType, Transaction, TxInfo}; use crate::crypto::bip32; use crate::crypto::ed25519::{Ed25519, Ed25519Trait}; -use crate::crypto::hash::{blake2b_digest, keccak256_digest}; -use crate::protos::generated::klv::proto; +use crate::crypto::hash::keccak256_digest; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; use bech32::{u5, Variant}; use crate::chains::util::private_key_from_vec; -use crate::crypto::base64::simple_base64_encode; -use crate::crypto::bignum::U256; -use prost::Message; const KLEVER_MESSAGE_PREFIX: &str = "\x17Klever Signed Message:\n"; @@ -84,31 +78,8 @@ impl Chain for KLV { private_key: Vec, mut tx: Transaction, ) -> Result { - let raw_tx = tx.raw_data; - - // 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)?; + let sig = self.sign_raw(private_key, tx.tx_hash.clone())?; - let raw_data = klv_tx - .raw_data - .clone() - .ok_or(ChainError::ProtoDecodeError)?; - let mut tx_raw = Vec::with_capacity(raw_data.encoded_len()); - raw_data.encode(&mut tx_raw)?; - let result_buffer = blake2b_digest(&tx_raw); - - let sig = self.sign_raw(private_key, result_buffer.to_vec())?; - - js_tx.signature = Some(Vec::from([simple_base64_encode(&sig)])); - - tx.raw_data = tiny_json_rs::encode(js_tx).into_bytes(); - tx.tx_hash = result_buffer.to_vec(); tx.signature = sig.as_slice().to_vec(); Ok(tx) } @@ -131,41 +102,8 @@ impl Chain for KLV { Ok(signature) } - fn get_tx_info(&self, raw_tx: Vec) -> Result { - let js_tx: models::Transaction = tiny_json_rs::decode(String::from_utf8(raw_tx)?)?; - let tx = proto::Transaction::try_from(js_tx).map_err(|_| ChainError::ProtoDecodeError)?; - let raw = tx.raw_data.ok_or(ChainError::ProtoDecodeError)?; - - if raw.contract.len() != 1 { - return Err(ChainError::ProtoDecodeError); - } - - let c_type: proto::tx_contract::ContractType = - proto::tx_contract::ContractType::try_from(raw.contract[0].r#type) - .map_err(|_| ChainError::ProtoDecodeError)?; - match c_type { - proto::tx_contract::ContractType::TransferContractType => { - let value = raw.contract[0].clone().parameter.unwrap().value; - let tc = proto::TransferContract::decode(value.as_slice()) - .map_err(|_| ChainError::ProtoDecodeError)?; - let sender = self.get_address(raw.sender)?; - let receiver = self.get_address(tc.to_address)?; - let value = U256::from_i64(tc.amount).to_f64(self.get_decimals()); - - Ok(TxInfo { - sender, - receiver, - value, - tx_type: TxType::Transfer, - }) - } - _ => Ok(TxInfo { - sender: String::new(), - receiver: String::new(), - value: 0.0, - tx_type: TxType::Unknown, - }), - } + fn get_tx_info(&self, _raw_tx: Vec) -> Result { + Err(ChainError::NotSupported) } fn get_chain_type(&self) -> ChainType { @@ -177,8 +115,6 @@ impl Chain for KLV { mod test { use crate::chains::Chain; use alloc::string::{String, ToString}; - use alloc::vec; - use alloc::vec::Vec; #[test] fn test_derive() { @@ -249,181 +185,6 @@ mod test { assert_eq!(signature.len(), 64) } - #[test] - fn test_sign_tx() { - let pvk = hex::decode("1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727") - .unwrap(); - - let raw_tx = hex::decode( - "7b2252617744617461223a7b224e6f6e6365223a3837312c2253656e646572223a226e506832763367457a636d41684b4c783630764d41734e65384a5871716f5a47695a30504e51434c2b55303d222c22436f6e7472616374223a5b7b22506172616d65746572223a7b22747970655f75726c223a22747970652e676f6f676c65617069732e636f6d2f70726f746f2e5472616e73666572436f6e7472616374222c2276616c7565223a224369417633486c46453731646170613948454e704a454b4671656e7468417a32306b67436c76776e46753076635249714d48686b59574d784e3259354e54686b4d6d566c4e54497a595449794d4459794d4459354f5451314f54646a4d544e6b4f444d785a574d3347416f3d227d7d5d2c224b417070466565223a313030303030302c2242616e647769647468466565223a323030303030302c2256657273696f6e223a312c22436861696e4944223a224d544134227d7d", - ) - .unwrap(); - - 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("0f47f28830f7aa9607a7a462b267003f94b4ef2c5c28ac8763cfc68e8fe10915") - .unwrap() - ) - } - - #[test] - fn test_sign_tx_2() { - let pvk = hex::decode("1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727") - .unwrap(); - - let raw_tx = hex::decode( - "7b2252617744617461223a7b224e6f6e6365223a3536392c2253656e646572223a2253715146557a4c44745a4e7a58657865592b2b424d56483547544b4c444d53786f3732476f52716a5a7a303d222c22436f6e7472616374223a5b7b2254797065223a392c22506172616d65746572223a7b22747970655f75726c223a22747970652e676f6f676c65617069732e636f6d2f70726f746f2e436c61696d436f6e7472616374227d7d5d2c224b417070466565223a313030303030302c2242616e647769647468466565223a323030303030302c2256657273696f6e223a312c22436861696e4944223a224d544134227d7d", - ) - .unwrap(); - - 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("a4f4768ef619999241cb6c81fed8affc8dbaaa32dd4ded674273c8b6f06ddf93") - .unwrap() - ); - } - - #[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_sign_tx_4() { - let pvk = hex::decode("1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727") - .unwrap(); - - let json = r#"{"RawData":{"Sender":"UMjR49Dkn+HleedQY88TSjXXJhtbDpX7f7QVF/Dcqos=","Contract":[{"Type":63,"Parameter":{"type_url":"type.googleapis.com/proto.SmartContract","value":"EiAAAAAAAAAAAAUAIPnuq04LIuz1ew83LbqEVgLiyNyybBoRCghGUkctMlZCVRIFCIDh6xc="}}],"Data":["c3Rha2VGYXJt"],"KAppFee":2000000,"BandwidthFee":4622449,"Version":1,"ChainID":"MTAwMDAx"}}"#; - - 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("50fce82cb3f4bf851d86fd594133b13e891d1f565958b0a95b11ce47f2179926") - .unwrap() - ); - } - #[test] - fn test_sign_tx_5() { - let pvk = hex::decode("1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727") - .unwrap(); - - let json = r#" - { - "RawData": { - "Nonce": 606, - "Sender": "SqQFUzLDtZNzXexeY++BMVH5GTKLDMSxo72GoRqjZz0=", - "Contract": [ - { - "Parameter": { - "type_url": "type.googleapis.com/proto.TransferContract", - "value": "CiBI/KqGTZBj8/J8YhvX20xb9NP6l5eXRCBmJsWERlhmSRIDS0xWGMCEPQ==" - } - } - ], - "Data": [ - "" - ], - "KAppFee": 1000000, - "BandwidthFee": 2000000, - "Version": 1, - "ChainID": "MTA4", - "KDAFee": { - "KDA": "Q0hJUFMtMUdaUA==", - "Amount": 688200 - } - } - }"#; - 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, - vec![ - 60, 222, 134, 3, 87, 86, 2, 184, 223, 221, 53, 134, 54, 27, 20, 178, 197, 31, 20, - 66, 12, 107, 186, 61, 21, 82, 78, 66, 210, 190, 124, 194 - ] - ); - } - - #[test] - fn test_decode_klv_tx() { - let raw_tx = hex::decode( - "7b225261774\ - 4617461223a7b224e6f6e6365223a322c2253656e646572223a2231427\ - 673447457583848784162506664437742686b6956767378446637354e7a\ - 4d4f6c44727357377034633d222c22436f6e7472616374223a5b7b22506\ - 172616d65746572223a7b22747970655f75726c223a22747970652e676f\ - 6f676c65617069732e636f6d2f70726f746f2e5472616e73666572436f6\ - e7472616374222c2276616c7565223a224369446653574f374e61687538\ - 74717056506b4d547645324a6a4649385752702f4d62452f326c702b385\ - 06f37786742227d7d5d2c224b417070466565223a3530303030302c2242\ - 616e647769647468466565223a313030303030302c2256657273696f6e2\ - 23a312c22436861696e4944223a224d544134227d7d", - ) - .unwrap(); - - let tx_info = crate::chains::klv::KLV {}.get_tx_info(raw_tx).unwrap(); - assert_eq!( - tx_info.sender, - "klv16sd7crk4jlc8csrv7lwskqrpjgjklvcsmlhexuesa9p6a3dm57rs5vh0hq" - ); - } - #[test] fn test_sign_message() { let mnemonic = diff --git a/packages/kos/src/chains/mod.rs b/packages/kos/src/chains/mod.rs index 479f038f..12c7831b 100644 --- a/packages/kos/src/chains/mod.rs +++ b/packages/kos/src/chains/mod.rs @@ -1,4 +1,3 @@ -use crate::alloc::borrow::ToOwned; use crate::chains::ada::ADA; use crate::chains::apt::APT; use crate::chains::atom::ATOM; @@ -18,16 +17,11 @@ use crate::crypto::secp256k1::Secp256Err; use crate::crypto::sr25519::Sr25519Error; use alloc::boxed::Box; use alloc::format; -use alloc::string::{FromUtf8Error, String, ToString}; +use alloc::string::{FromUtf8Error, String}; use alloc::vec::Vec; use core::fmt::Display; use prost::{DecodeError, EncodeError}; use rlp::DecoderError; -use tiny_json_rs::lexer::StringType; -use tiny_json_rs::mapper; -use tiny_json_rs::serializer; -use tiny_json_rs::serializer::Token; -use tiny_json_rs::Serialize; pub mod ada; pub mod apt; @@ -242,12 +236,6 @@ impl From for ChainError { } } -impl From for ChainError { - fn from(_: serializer::DecodeError) -> Self { - ChainError::ProtoDecodeError - } -} - impl From for ChainError { fn from(value: Sr25519Error) -> Self { ChainError::CurveErrorSr(value) @@ -304,23 +292,6 @@ pub enum TxType { TriggerContract, } -impl serializer::Serialize for TxType { - fn serialize(&self) -> mapper::Value { - let str = match self { - TxType::Unknown => "Unknown", - TxType::Transfer => "Transfer", - TxType::TriggerContract => "TriggerContract", - }; - let token = Token { - token_type: tiny_json_rs::lexer::TokenType::String(StringType::SimpleString), - literal: str.to_string(), - }; - - mapper::Value::Token(token) - } -} - -#[derive(Serialize)] pub struct TxInfo { pub sender: String, pub receiver: String, diff --git a/packages/kos/src/chains/sol/mod.rs b/packages/kos/src/chains/sol/mod.rs index f6c4c468..a6400059 100644 --- a/packages/kos/src/chains/sol/mod.rs +++ b/packages/kos/src/chains/sol/mod.rs @@ -1,13 +1,11 @@ -mod models; - use crate::chains::util::private_key_from_vec; use crate::chains::{Chain, ChainError, ChainType, Transaction, TxInfo}; use crate::crypto::b58::b58enc; use crate::crypto::bip32; use crate::crypto::ed25519::{Ed25519, Ed25519Trait}; + use alloc::format; use alloc::string::String; -use alloc::vec; use alloc::vec::Vec; #[allow(clippy::upper_case_acronyms)] @@ -60,33 +58,8 @@ impl Chain for SOL { private_key: Vec, mut tx: Transaction, ) -> Result { - let mut sol_tx = models::SolanaTransaction::decode(&tx.raw_data)?; - - if sol_tx.message.header.num_required_signatures as usize != 1 { - return Err(ChainError::InvalidTransactionHeader); - } - if sol_tx.message.account_keys.is_empty() { - return Err(ChainError::InvalidAccountLength); - } - if sol_tx.message.recent_blockhash.iter().all(|&x| x == 0) - || sol_tx.message.recent_blockhash.iter().all(|&x| x == 1) - { - return Err(ChainError::InvalidBlockhash); - } - - let message_bytes = sol_tx.message.encode()?; - - let signature = self.sign_raw(private_key, message_bytes)?; - if signature.len() != 64 { - return Err(ChainError::InvalidSignatureLength); - } - sol_tx.signatures = vec![signature.clone()]; - - tx.tx_hash = sol_tx.signatures[0].clone(); - - let signed_tx = sol_tx.encode()?; + let signature = self.sign_raw(private_key, tx.tx_hash.clone())?; - tx.raw_data = signed_tx; tx.signature = signature; Ok(tx) } @@ -119,7 +92,6 @@ impl Chain for SOL { #[cfg(test)] mod test { use super::*; - use crate::crypto::base64::simple_base64_decode; use alloc::string::ToString; #[test] @@ -135,136 +107,6 @@ mod test { assert_eq!(addr, "B9sVeu4rJU12oUrUtzjc6BSNuEXdfvurZkdcaTVkP2LY"); } - fn create_test_transaction() -> Vec { - let tx = models::SolanaTransaction { - message: models::Message { - version: "legacy".to_string(), - header: models::MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - account_keys: vec![ - vec![1; 32], // Sender - vec![2; 32], // Recipient - vec![3; 32], // Program ID - ], - recent_blockhash: [42; 32], - instructions: vec![models::CompiledInstruction { - program_id_index: 2, - accounts: vec![0, 1], - data: vec![2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0], // Transfer 100 lamports - }], - address_table_lookups: vec![], - }, - signatures: vec![], - }; - tx.encode().unwrap() - } - - #[test] - fn test_derive_and_sign_tx() { - let sol = SOL {}; - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap(); - let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap(); - - let raw_tx = create_test_transaction(); - let tx = Transaction { - raw_data: raw_tx, - tx_hash: vec![], - signature: vec![], - options: Option::None, - }; - - let result = sol.sign_tx(pvk, tx).unwrap(); - - assert_eq!(result.signature.len(), 64); - - // Verify tx_hash is not all ones and matches signature - assert!(!result.tx_hash.iter().all(|&x| x == 1)); - assert_eq!(result.tx_hash.len(), 64); - assert!(!result.tx_hash.iter().all(|&x| x == 1)); - assert!(!result.tx_hash.iter().all(|&x| x == 0)); - - let decoded = models::SolanaTransaction::decode(&result.raw_data).unwrap(); - assert_eq!(decoded.signatures.len(), 1); - assert_eq!(decoded.signatures[0], result.signature); - assert_eq!(decoded.message.header.num_required_signatures, 1); - } - - #[test] - fn test_sign_tx_consistent_hash() { - let sol = SOL {}; - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap(); - let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap(); - - let raw_tx = create_test_transaction(); - let tx1 = Transaction { - raw_data: raw_tx.clone(), - tx_hash: vec![], - signature: vec![], - options: Option::None, - }; - - let tx2 = Transaction { - raw_data: raw_tx, - tx_hash: vec![], - signature: vec![], - options: Option::None, - }; - - let result1 = sol.sign_tx(pvk.clone(), tx1).unwrap(); - let result2 = sol.sign_tx(pvk, tx2).unwrap(); - - // Same transaction signed with same key should produce same signature and hash - assert_eq!(result1.signature, result2.signature); - assert_eq!(result1.tx_hash, result2.tx_hash); - } - - #[test] - fn test_sign_tx_legacy() { - let sol = SOL {}; - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap(); - let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap(); - - let raw_tx = simple_base64_decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIEmjxocK65Bo8r+e3cj7GbPVedpCwx+DCZJ57Tw3fMN0e5dTAYLc651CwBwFga8GLJTsriJc/FAP3GlbhfEGOidAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAACg2vm5+lhfRud/PKY6hEMgdKkQ8I7jtpxDFjknIKRXGQMDAAUCSQIAAAMACQOAlpgAAAAAAAICAAEUAgAAAAEAAAAAAAAAsmBySL6HLBg=").unwrap(); - - let tx1 = Transaction { - raw_data: raw_tx.clone(), - tx_hash: vec![], - signature: vec![], - options: Option::None, - }; - - let result = sol.sign_tx(pvk.clone(), tx1).unwrap(); - - // Same transaction signed with same key should produce same signature and hash - assert_eq!(hex::encode(&result.signature), "b079c666c9ff53bb26d7606d10131ebbc8d398dac9fd1285d5138bbdd521758d7a6b6bdb2876730637704eb1511f3f7d842343b9e406bb3e3583d6588949a904"); - } - - #[test] - fn test_sign_tx_v0() { - let sol = SOL {}; - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap(); - let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap(); - - let raw_tx = simple_base64_decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAGCpo8aHCuuQaPK/nt3I+xmz1XnaQsMfgwmSee08N3zDdHWO9nf7VjXmRzcktw4WtkBVQDTqR6HHs/zYiFPEFdMlR2uAUKvCmGoT5EOvm/TqTTENr0znYcEsWsViKudXw20rGZQgJtALiRcUwlRMT2kZt8QRbvckZEPIiyFe59326vAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsH4P9uc5VDeldVYzceVRhzPQ3SsaI7BOphAAiCnjaBgMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAtD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5egEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpheXoR6gYqo7X4aA9Sx2/Qcpf6TpzF6ddVuj771s5eWQFBgAFAua+AQAGAAkDRJEGAAAAAAAIBQMAEwkECZPxe2T0hK52/wgYCQACAwgTAQcIDxELAAIDDgoNDAkSEhAFI+UXy5d6460qAQAAABlkAAH4LgEAAAAAAMGtCQAAAAAAKwAFCQMDAAABCQEP5d+hcffknhCj1qkbVbtXFKZDtelOHlry/os01b5PsgXi4ePoyQXn5ODlRQ==").unwrap(); - let tx1 = Transaction { - raw_data: raw_tx.clone(), - tx_hash: vec![], - signature: vec![], - options: Option::None, - }; - - let result = sol.sign_tx(pvk.clone(), tx1).unwrap(); - // Same transaction signed with same key should produce same signature and hash - assert_eq!(hex::encode(&result.signature), "40098643a37209b2e0984c2f55872ccf150c44a1100a16a985b1bc04b13c31f9d9d1b070229241df5aaa21af22e0e4f88b6371106766fd95096b67f1066f8701"); - } - #[test] fn test_sign_message() { let sol = SOL {}; diff --git a/packages/kos/src/chains/substrate/mod.rs b/packages/kos/src/chains/substrate/mod.rs index e602df4d..ad301b80 100644 --- a/packages/kos/src/chains/substrate/mod.rs +++ b/packages/kos/src/chains/substrate/mod.rs @@ -1,16 +1,11 @@ -mod models; - -use crate::chains::substrate::models::ExtrinsicPayload; use crate::chains::util::private_key_from_vec; -use crate::chains::{Chain, ChainError, ChainOptions, ChainType, Transaction, TxInfo}; -use crate::crypto::hash::{blake2b_64_digest, blake2b_digest}; +use crate::chains::{Chain, ChainError, ChainType, Transaction, TxInfo}; +use crate::crypto::hash::blake2b_64_digest; use crate::crypto::sr25519::Sr25519Trait; use crate::crypto::{b58, bip32, sr25519}; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; -use models::{Call, CallArgs}; -use parity_scale_codec::Decode; const LOWER_MASK: u16 = 0x3FFF; const TYPE1_ACCOUNT_ID: u16 = 63; @@ -110,77 +105,7 @@ impl Chain for Substrate { private_key: Vec, mut tx: Transaction, ) -> Result { - let options = tx.options.clone().ok_or(ChainError::MissingOptions)?; - - let extrinsic = match options { - ChainOptions::SUBSTRATE { - call, - era, - nonce, - tip, - block_hash, - genesis_hash, - spec_version, - transaction_version, - app_id, - } => { - let genesis_hash: [u8; 32] = genesis_hash - .as_slice() - .try_into() - .map_err(|_| ChainError::InvalidOptions)?; - - let block_hash: [u8; 32] = block_hash - .as_slice() - .try_into() - .map_err(|_| ChainError::InvalidOptions)?; - - // Other chains may have different requirements for mode and metadata_hash - let (mode, metadata_hash) = match self.symbol.as_str() { - "REEF" => (None, None), - _ => (Some(0u8), Some(0u8)), - }; - - ExtrinsicPayload { - call, - era, - nonce, - tip, - mode, - spec_version, - transaction_version, - genesis_hash, - block_hash, - metadata_hash, - app_id, - } - } - _ => { - return Err(ChainError::InvalidOptions); - } - }; - - let signature = { - let full_unsigned_payload_scale_bytes = extrinsic.to_bytes(); - - // If payload is longer than 256 bytes, we hash it and sign the hash instead: - if full_unsigned_payload_scale_bytes.len() > 256 { - self.sign_raw( - private_key.clone(), - blake2b_digest(&full_unsigned_payload_scale_bytes).to_vec(), - )? - } else { - self.sign_raw(private_key.clone(), full_unsigned_payload_scale_bytes)? - } - }; - - let pbk_vec = self.get_pbk(private_key)?; - let public_key: [u8; 32] = pbk_vec - .try_into() - .map_err(|_| ChainError::InvalidPublicKey)?; - - tx.raw_data = extrinsic.encode_with_signature(&public_key, &signature); - - tx.signature = [[1u8].to_vec(), signature].concat(); + tx.signature = self.sign_raw(private_key.clone(), tx.tx_hash.clone())?; Ok(tx) } @@ -201,18 +126,7 @@ impl Chain for Substrate { } fn get_tx_info(&self, _raw_tx: Vec) -> Result { - let info = Call::decode(&mut &_raw_tx[..]).map_err(|_| ChainError::InvalidPrivateKey)?; - let args = info.args.to_vec(); - let tx_info = - CallArgs::decode(&mut &args[..]).map_err(|_| ChainError::InvalidPrivateKey)?; - let address_to = self.get_address(tx_info.addr_to.to_vec())?; - let new_tx_info = TxInfo { - receiver: address_to, - sender: "".to_string(), - tx_type: super::TxType::Unknown, - value: tx_info.amount.to_f64(self.get_decimals()), - }; - Ok(new_tx_info) + Err(ChainError::NotSupported) } fn get_chain_type(&self) -> ChainType { @@ -222,8 +136,7 @@ impl Chain for Substrate { #[cfg(test)] mod test { - use crate::chains::{Chain, ChainOptions, Transaction}; - use crate::crypto::base64::simple_base64_decode; + use crate::chains::{Chain, ChainOptions}; use alloc::string::{String, ToString}; use alloc::vec::Vec; use schnorrkel; @@ -344,375 +257,6 @@ mod test { assert_eq!(sig.len(), 64); } - #[test] - fn sign_tx_1() { - let dot = super::Substrate::new(21, 0, "Polkadot", "DOT"); - - let mnemonic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let path = dot.get_path(0, false); - - let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); - let pvk = dot.derive(seed, path).unwrap(); - - let raw_data = simple_base64_decode("BQMADCRBuM7b/Hou3AlouaU1gZlp0+ngmYaAurtYJyh/wHAE1QFsAAD8TQ8AGgAAAJGxcbsVji04SPojqfHCUYL7jiAxOywetJIZ2npwzpDDR+4cSO05ZyHnTfHIHpWyqrPhN2Poot7H7VqLlK9MgI0A").unwrap(); - - let options = ChainOptions::SUBSTRATE { - call: hex::decode( - "0503000c2441b8cedbfc7a2edc0968b9a535819969d3e9e0998680babb5827287fc07004", - ) - .unwrap(), - era: hex::decode("d501").unwrap(), - nonce: 27, - tip: 0, - block_hash: hex::decode( - "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", - ) - .unwrap(), - genesis_hash: hex::decode( - "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", - ) - .unwrap(), - spec_version: 1003004, - transaction_version: 26, - app_id: None, - }; - - let tx = Transaction { - raw_data, - signature: Vec::new(), - tx_hash: Vec::new(), - options: Some(options), - }; - - let signed_tx = dot.sign_tx(pvk, tx).unwrap(); - - assert_eq!(signed_tx.signature.len(), 65); - assert_eq!(signed_tx.raw_data.len(), 142); - } - - #[test] - fn sign_tx_2() { - let dot = super::Substrate::new(27, 2, "Kusama", "KSM"); - - let mnemonic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let path = dot.get_path(0, false); - - let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); - let pvk = dot.derive(seed, path).unwrap(); - - let raw_data = simple_base64_decode("BgMATg7dBMR7Gtw7IdzYZxpdkKHC63X7YNKTqQhvJibbzVkEJQEcAAAoAAAAAQAAALkXRrReA0bML4FaUgucbLTVwJAq+EjbCoD4WTLS6CdqrE2opg7XFpDCJ63rn+zxU3cs7DhW6Sm5cCF02Gg1wDY=").unwrap(); - - let options = ChainOptions::SUBSTRATE { - call: hex::decode( - "0403004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904", - ) - .unwrap(), - era: hex::decode("4502").unwrap(), - nonce: 87, - tip: 0, - block_hash: hex::decode( - "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", - ) - .unwrap(), - genesis_hash: hex::decode( - "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", - ) - .unwrap(), - spec_version: 1003003, - transaction_version: 26, - app_id: None, - }; - - let tx = Transaction { - raw_data, - signature: Vec::new(), - tx_hash: Vec::new(), - options: Some(options), - }; - - let signed_tx = dot.sign_tx(pvk, tx).unwrap(); - - assert_eq!(signed_tx.signature.len(), 65); - assert_eq!(signed_tx.raw_data.len(), 143); - } - - #[test] - fn sign_tx_3() { - let dot = super::Substrate::new(62, 42, "Avail", "AVAIL"); - - let mnemonic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let path = dot.get_path(0, false); - - let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); - let pvk = dot.derive(seed, path).unwrap(); - - let raw_data = simple_base64_decode("BgMATg7dBMR7Gtw7IdzYZxpdkKHC63X7YNKTqQhvJibbzVkEtQEgAAAoAAAAAQAAALkXRrReA0bML4FaUgucbLTVwJAq+EjbCoD4WTLS6CdqDhX+2GUB2kR8rjtzYfwUoIfzCa63UQhdcamIqku0qBE=").unwrap(); - - let nonce = u32::from_str_radix("0x00000008".trim_start_matches("0x"), 16).unwrap(); - let spec_version = u32::from_str_radix("0x00000028".trim_start_matches("0x"), 16).unwrap(); - let transaction_version = - u32::from_str_radix("0x00000001".trim_start_matches("0x"), 16).unwrap(); - - let options = ChainOptions::SUBSTRATE { - call: hex::decode( - "0603004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904", - ) - .unwrap(), - era: hex::decode("b501").unwrap(), - nonce, - tip: 0, - block_hash: hex::decode( - "0e15fed86501da447cae3b7361fc14a087f309aeb751085d71a988aa4bb4a811", - ) - .unwrap(), - genesis_hash: hex::decode( - "b91746b45e0346cc2f815a520b9c6cb4d5c0902af848db0a80f85932d2e8276a", - ) - .unwrap(), - spec_version, - transaction_version, - app_id: Some(0), - }; - - let tx = Transaction { - raw_data, - signature: Vec::new(), - tx_hash: Vec::new(), - options: Some(options), - }; - - let signed_tx = dot.sign_tx(pvk, tx).unwrap(); - - assert_eq!(signed_tx.signature.len(), 65); - assert_eq!(signed_tx.raw_data.len(), 142); - } - - #[test] - fn sign_tx_4() { - let dot = super::Substrate::new(29, 42, "REEF", "Reef"); - - let mnemonic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let path = dot.get_path(1, false); - - let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); - let pvk = dot.derive(seed, path).unwrap(); - - let raw_data = simple_base64_decode("BgMAIBAGX9aAF/hRd/ms+YCatuNZ7HYxyfly/j75aXrwkxMEFQNgAAoAAAACAAAAeDR4HTjkeY1UjjTslH0Z3uop3xSKe/MkhLeyTaz41LdWfCQku+9zEoyAsxnOxPxhQOEisj7yIJbyvkGmUcrXaw==").unwrap(); - - let nonce = 24; - let spec_version = 10; - let transaction_version = 2; - - let options = ChainOptions::SUBSTRATE { - call: hex::decode( - "0603002010065fd68017f85177f9acf9809ab6e359ec7631c9f972fe3ef9697af0931304", - ) - .unwrap(), - era: hex::decode("1503").unwrap(), - nonce, - tip: 0, - block_hash: hex::decode( - "567c2424bbef73128c80b319cec4fc6140e122b23ef22096f2be41a651cad76b", - ) - .unwrap(), - genesis_hash: hex::decode( - "7834781d38e4798d548e34ec947d19deea29df148a7bf32484b7b24dacf8d4b7", - ) - .unwrap(), - spec_version, - transaction_version, - app_id: None, - }; - - let tx = Transaction { - raw_data, - signature: Vec::new(), - tx_hash: Vec::new(), - options: Some(options), - }; - - let signed_tx = dot.sign_tx(pvk, tx).unwrap(); - - assert_eq!(signed_tx.signature.len(), 65); - assert_eq!(signed_tx.raw_data.len(), 142); - } - - #[test] - fn sign_tx_5() { - let dot = super::Substrate::new(41, 8, "Karura", "KAR"); - - let mnemonic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let path = dot.get_path(0, false); - - let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); - let pvk = dot.derive(seed, path).unwrap(); - - let pbk = dot.get_pbk(pvk.clone()).unwrap(); - - let raw_data = simple_base64_decode("CgMAbW9kbGFjYS9pbmN0AAAAAAAAAAAAAAAAAAAAAAAAAAAE9QIAAADoCAAAAgAAALr1qr5AZG0R8O6Ku9xk9KS3Z0kly6COSgX/nr7W4hJrTimIjSb829wZAW19nqKqT5jkpTo80WAgCLqC3vJu6ycA").unwrap(); - - let nonce = 0; - let spec_version = 2280; - let transaction_version = 2; - - let options = ChainOptions::SUBSTRATE { - call: hex::decode( - "0a03006d6f646c6163612f696e6374000000000000000000000000000000000000000004", - ) - .unwrap(), - era: hex::decode("f502").unwrap(), - nonce, - tip: 0, - block_hash: hex::decode( - "4e29888d26fcdbdc19016d7d9ea2aa4f98e4a53a3cd1602008ba82def26eeb27", - ) - .unwrap(), - genesis_hash: hex::decode( - "baf5aabe40646d11f0ee8abbdc64f4a4b7674925cba08e4a05ff9ebed6e2126b", - ) - .unwrap(), - spec_version, - transaction_version, - app_id: None, - }; - - let tx = Transaction { - raw_data, - signature: Vec::new(), - tx_hash: Vec::new(), - options: Some(options), - }; - - let signed_tx = dot.sign_tx(pvk, tx).unwrap(); - - assert_eq!(signed_tx.signature.len(), 65); - assert_eq!(signed_tx.raw_data.len(), 142); - } - #[test] - fn sign_tx_6() { - let dot = super::Substrate::new(46, 10, "Acala", "ACA"); - - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let path = dot.get_path(0, false); - - let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); - let pvk = dot.derive(seed, path).unwrap(); - let pbk = dot.get_pbk(pvk.clone()).unwrap(); - - let raw_data = simple_base64_decode("CgMAbW9kbGFjYS9kZXhtAAAAAAAAAAAAAAAAAAAAAAAAAAAEBQPVTQAA6AgAAAMAAAD8Qbm9jvj+U9WMfqZ8eUx+yac9rwXm1UsU/2NCyZumTMZAZ+YgN3HGoPCoy9HNtxDCqeRTcz9HtiAUy505IgcjAA==").unwrap(); - - let nonce = 4981; - let spec_version = 2280; - let transaction_version = 3; - - let options = ChainOptions::SUBSTRATE { - call: hex::decode( - "0a03006d6f646c6163612f6465786d000000000000000000000000000000000000000004", - ) - .unwrap(), - era: hex::decode("0503").unwrap(), - nonce, - tip: 0, - block_hash: hex::decode( - "c64067e6203771c6a0f0a8cbd1cdb710c2a9e453733f47b62014cb9d39220723", - ) - .unwrap(), - genesis_hash: hex::decode( - "fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c", - ) - .unwrap(), - spec_version, - transaction_version, - app_id: None, - }; - - let tx = Transaction { - raw_data, - signature: Vec::new(), - tx_hash: Vec::new(), - options: Some(options), - }; - - let signed_tx = dot.sign_tx(pvk, tx).unwrap(); - - assert_eq!(signed_tx.signature.len(), 65); - assert_eq!(signed_tx.raw_data.len(), 143); - } - - #[test] - fn sign_tx_browser() { - let dot = super::Substrate::new(21, 0, "Polkadot", "DOT"); - - let mnemonic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let path = dot.get_path(1, false); - - let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); - let pvk = dot.derive(seed, path).unwrap(); - - let raw_data = simple_base64_decode("BQMADCRBuM7b/Hou3AlouaU1gZlp0+ngmYaAurtYJyh/wHAE1QFsAAD8TQ8AGgAAAJGxcbsVji04SPojqfHCUYL7jiAxOywetJIZ2npwzpDDR+4cSO05ZyHnTfHIHpWyqrPhN2Poot7H7VqLlK9MgI0A").unwrap(); - - let options = options_from_browser_json( - r#"{ - "specVersion": "0x000f4dfc", - "transactionVersion": "0x0000001a", - "address": "12mM9imBfhL4DfK2Sv9SPi79kKT296YJ6LTT7b7pZuRufXmx", - "assetId": null, - "blockHash": "0x74e061f402b8709b793aede7509ac32ca2bf60ab772035e8de2c3b597a99c3cd", - "blockNumber": "0x017870fe", - "era": "0xe503", - "genesisHash": "0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", - "metadataHash": null, - "method": "0x0503004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5900", - "mode": 0, - "nonce": "0x00000000", - "signedExtensions": [ - "CheckNonZeroSender", - "CheckSpecVersion", - "CheckTxVersion", - "CheckGenesis", - "CheckMortality", - "CheckNonce", - "CheckWeight", - "ChargeTransactionPayment", - "PrevalidateAttests", - "CheckMetadataHash" - ], - "tip": "0x00000000000000000000000000000000", - "version": 4, - "withSignedTransaction": true - }"# - .to_string(), - ); - - let tx = Transaction { - raw_data, - signature: Vec::new(), - tx_hash: Vec::new(), - options: Some(options), - }; - - let signed_tx = dot.sign_tx(pvk, tx).unwrap(); - - assert_eq!(signed_tx.signature.len(), 65); - assert_eq!(signed_tx.raw_data.len(), 142); - } - - #[test] - fn test_get_tx_info() { - let dot = super::Substrate::new(21, 0, "Polkadot", "DOT"); - let raw_data = "05030092fb4dc4e0790663aa4be18e6c49d62e8db091a6e1c4d0727c14906cf79f0f7a280000002b460f001900000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c391b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3".to_string(); - - let raw_data_hex = hex::decode(raw_data).unwrap(); - let tx_info = dot.get_tx_info(raw_data_hex); - assert!(tx_info.is_ok()); - } - #[test] fn test_sign_message() { let dot = super::Substrate::new(21, 0, "Polkadot", "DOT"); diff --git a/packages/kos/src/chains/substrate/models.rs b/packages/kos/src/chains/substrate/models.rs deleted file mode 100644 index 3bece52f..00000000 --- a/packages/kos/src/chains/substrate/models.rs +++ /dev/null @@ -1,227 +0,0 @@ -use crate::crypto::bignum::U256; -use alloc::vec; -use alloc::vec::Vec; -use parity_scale_codec::{Compact, Decode, Encode, Input}; - -const SIGNED_FLAG: u8 = 0b1000_0000; -const TRANSACTION_VERSION: u8 = 4; -const PUBLIC_KEY_TYPE: u8 = 0x00; -const SIGNATURE_TYPE: u8 = 0x01; - -#[derive(Decode)] -pub struct Call { - pub _call_index: CallIndex, - pub args: [u8; 100], -} - -#[derive(Decode)] -pub struct CallIndex { - _section_index: u8, - _method_index: u8, -} - -#[derive(Decode)] -pub struct CallArgs { - pub addr_to: MultiAddress, - pub amount: UIntCompact, -} - -pub struct MultiAddress { - pub is_id: bool, - pub as_id: [u8; 32], - pub is_index: bool, - pub as_index: u32, - pub is_raw: bool, - pub as_raw: [u8; 32], - pub is_address32: bool, - pub as_address32: [u8; 32], - pub is_address20: bool, - pub as_address20: [u8; 20], -} - -impl MultiAddress { - pub fn to_vec(&self) -> Vec { - if self.is_id { - return self.as_id.to_vec(); - } - if self.is_index { - return self.as_index.to_le_bytes().to_vec(); - } - if self.is_raw { - return self.as_raw.to_vec(); - } - if self.is_address32 { - return self.as_address32.to_vec(); - } - self.as_address20.to_vec() - } -} - -impl Decode for MultiAddress { - fn decode(input: &mut I) -> Result { - let mut address = MultiAddress { - is_id: false, - as_id: [0; 32], - is_index: false, - as_index: 0, - is_raw: false, - as_raw: [0; 32], - is_address32: false, - as_address32: [0; 32], - is_address20: false, - as_address20: [0; 20], - }; - let t = input.read_byte()?; - - match t { - 1 => { - address.is_index = true; - let mut as_index = [0u8; 4]; - let _ = input.read(&mut as_index); - address.as_index = u32::from_le_bytes(as_index); - Ok(address) - } - 2 => { - address.is_raw = true; - let _ = input.read(&mut address.as_raw); - Ok(address) - } - 3 => { - address.is_address32 = true; - let _ = input.read(&mut address.as_address32); - Ok(address) - } - 4 => { - address.is_address20 = true; - let _ = input.read(&mut address.as_address20); - Ok(address) - } - _ => { - address.is_id = true; - let _ = input.read(&mut address.as_id); - Ok(address) - } - } - } -} - -pub type UIntCompact = U256; - -impl Decode for UIntCompact { - fn decode(input: &mut I) -> Result { - let b = input.read_byte()?; - let mode = b & 3; - - match mode { - 1 => { - let bb = input.read_byte()?; - let mut r = bb as u64; - r <<= 6; - r += (b >> 2) as u64; - Ok(U256::from_u64(r)) - } - 2 => { - let mut buf = [0u8; 4]; - let _ = input.read(&mut buf); - let mut r = u32::from_le_bytes(buf); - r >>= 2; - Ok(U256::from_u64((r) as u64)) - } - 3 => { - let l = b >> 2; - if l > 63 { - todo!() - } - let mut buf = vec![0u8; (l + 4) as usize]; - input.read(&mut buf)?; - Ok(U256::read_data_as_le(buf)) - } - _ => Ok(U256::from_u64((b >> 2) as u64)), - } - } -} - -/// Represents the payload of a Substrate extrinsic (transaction) that will be signed. -/// This structure contains all the necessary fields required for transaction signing. -#[allow(dead_code)] -pub struct ExtrinsicPayload { - pub call: Vec, - pub era: Vec, - pub nonce: u32, - pub tip: u8, - pub mode: Option, - pub spec_version: u32, - pub transaction_version: u32, - pub genesis_hash: [u8; 32], - pub block_hash: [u8; 32], - pub metadata_hash: Option, - pub app_id: Option, -} - -impl ExtrinsicPayload { - /// Encodes the payload using the Substrate transaction format. - /// The format is: version + era + nonce + tip + call + params - pub fn to_bytes(&self) -> Vec { - let mut encoded = Vec::new(); - encoded.extend(self.call.clone()); - encoded.extend(&self.era.clone()); - encoded.extend(Compact(self.nonce).encode()); - encoded.extend(Compact(self.tip).encode()); - - // Use the app_id if it is set for AVAIL transactions, otherwise use the mode - if let Some(app_id) = self.app_id { - encoded.extend(Compact(app_id).encode()); - } else if let Some(mode) = self.mode { - encoded.extend(mode.encode()); - } - - encoded.extend(&self.spec_version.encode()); - encoded.extend(&self.transaction_version.encode()); - encoded.extend(&self.genesis_hash); - encoded.extend(&self.block_hash); - - // Use the metadata_hash if it is not set for AVAIL transactions - if self.app_id.is_none() { - if let Some(metadata_hash) = self.metadata_hash { - encoded.push(metadata_hash); - } - } - - encoded - } - - /// Encodes the payload with a signature using the Substrate transaction format. - /// The format is: length + (version + signature + era + nonce + tip + call + params) - pub fn encode_with_signature(&self, public_key: &[u8; 32], signature: &[u8]) -> Vec { - let mut encoded = Vec::new(); - - encoded.push(SIGNED_FLAG | TRANSACTION_VERSION); - - encoded.push(PUBLIC_KEY_TYPE); - encoded.extend_from_slice(public_key); - - encoded.push(SIGNATURE_TYPE); - - encoded.extend_from_slice(signature); - - encoded.extend_from_slice(&self.era); - encoded.extend_from_slice(&Compact(self.nonce).encode()); - encoded.extend_from_slice(&Compact(self.tip).encode()); - - // Use the app_id if it is set for AVAIL transactions, otherwise use the mode - if let Some(app_id) = self.app_id { - encoded.extend_from_slice(Compact(app_id).encode().as_slice()); - } else if let Some(mode) = self.mode { - encoded.push(mode); - } - - encoded.extend_from_slice(&self.call); - - let length = Compact(encoded.len() as u32).encode(); - let mut complete_encoded = Vec::with_capacity(length.len() + encoded.len()); - complete_encoded.extend_from_slice(&length); - complete_encoded.extend_from_slice(&encoded); - - complete_encoded - } -} diff --git a/packages/kos/src/chains/trx/mod.rs b/packages/kos/src/chains/trx/mod.rs index d366af64..1fe3fefa 100644 --- a/packages/kos/src/chains/trx/mod.rs +++ b/packages/kos/src/chains/trx/mod.rs @@ -1,16 +1,12 @@ use crate::chains::util::{private_key_from_vec, slice_from_vec}; -use crate::chains::{Chain, ChainError, ChainType, Transaction, TxInfo, TxType}; +use crate::chains::{Chain, ChainError, ChainType, Transaction, TxInfo}; use crate::crypto::b58::b58enc; -use crate::crypto::bignum::U256; use crate::crypto::hash::{keccak256_digest, sha256_digest}; use crate::crypto::secp256k1::Secp256k1Trait; use crate::crypto::{bip32, secp256k1}; -use crate::protos::generated::trx::protocol; -use crate::protos::generated::trx::protocol::transaction::contract::ContractType; +use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; -use alloc::{format, vec}; -use prost::Message; const TRX_ADDR_PREFIX: u8 = 0x41; const TRX_ADD_RAW_LEN: usize = 21; @@ -40,24 +36,6 @@ impl TRX { let bytes_addr = b58enc(&address_with_checksum[..]); String::from_utf8(bytes_addr).unwrap() } - - #[allow(clippy::single_match)] - pub fn decode_transaction(raw_tx: Vec) -> Result { - let tx = protocol::Transaction::decode(raw_tx.as_slice()); - match tx { - Ok(t) => return Ok(t), - Err(_) => {} - } - - let raw_tx = protocol::transaction::Raw::decode(raw_tx.as_slice())?; - let tx = protocol::Transaction { - raw_data: Some(raw_tx), - signature: vec![], - ret: vec![], - }; - - Ok(tx) - } } impl Chain for TRX { @@ -115,34 +93,18 @@ impl Chain for TRX { private_key: Vec, mut tx: Transaction, ) -> Result { - let raw_tx = tx.raw_data; - if private_key.len() != 32 { return Err(ChainError::InvalidPrivateKey); } let mut pvk_bytes: [u8; 32] = [0; 32]; pvk_bytes.copy_from_slice(&private_key[..32]); - let mut tron_tx = TRX::decode_transaction(raw_tx)?; - - let raw_data_clone = tron_tx - .raw_data - .clone() - .ok_or(ChainError::ProtoDecodeError)?; - let mut tx_raw = Vec::new(); - raw_data_clone.encode(&mut tx_raw)?; - let tx_id = sha256_digest(&tx_raw[..]); - let sig = secp256k1::Secp256K1::sign(&tx_id, &pvk_bytes)?; + let mut payload = [0u8; 32]; + payload.copy_from_slice(&tx.tx_hash[..]); - tron_tx.signature.push(sig.to_vec()); + tx.signature = secp256k1::Secp256K1::sign(&payload, &pvk_bytes)?.to_vec(); - let mut tx_data = Vec::new(); - tron_tx.encode(&mut tx_data)?; - - tx.raw_data = tx_data; - tx.tx_hash = tx_id.to_vec(); - tx.signature = sig.as_slice().to_vec(); Ok(tx) } @@ -174,111 +136,8 @@ impl Chain for TRX { Ok(sig.to_vec()) } - fn get_tx_info(&self, raw_tx: Vec) -> Result { - let tx = TRX::decode_transaction(raw_tx)?; - let raw = tx.raw_data.ok_or(ChainError::ProtoDecodeError)?; - if raw.contract.is_empty() { - return Err(ChainError::ProtoDecodeError); - } - - let contract = raw.contract[0].clone(); - - let contract_type = - ContractType::try_from(contract.r#type).map_err(|_| ChainError::ProtoDecodeError)?; - let parameter = contract.parameter.ok_or(ChainError::ProtoDecodeError)?; - - match contract_type { - ContractType::TransferContract => { - let transfer_contract = - protocol::TransferContract::decode(parameter.value.as_slice())?; - let mut owner_u8_addr: [u8; 21] = [0; 21]; - let mut owner_address = String::from(""); - - if transfer_contract.owner_address.len() >= TRX_ADD_RAW_LEN { - owner_u8_addr.copy_from_slice(&transfer_contract.owner_address[..]); - owner_address = TRX::expand_address_with_checksum(&owner_u8_addr); - } - - let mut to_u8_addr: [u8; 21] = [0; 21]; - let mut to_address = String::from(""); - - if transfer_contract.to_address.len() >= TRX_ADD_RAW_LEN { - to_u8_addr.copy_from_slice(&transfer_contract.to_address[..]); - to_address = TRX::expand_address_with_checksum(&to_u8_addr); - } - - let value = U256::from_i64(transfer_contract.amount).to_f64(self.get_decimals()); - return Ok(TxInfo { - sender: owner_address, - receiver: to_address, - value, - tx_type: TxType::Transfer, - }); - } - - ContractType::TransferAssetContract => { - let transfer_contract: protocol::TransferAssetContract = - protocol::TransferAssetContract::decode(parameter.value.as_slice())?; - let mut owner_u8_addr: [u8; 21] = [0; 21]; - let mut owner_address = String::from(""); - - if transfer_contract.owner_address.len() >= TRX_ADD_RAW_LEN { - owner_u8_addr.copy_from_slice(&transfer_contract.owner_address[..]); - owner_address = TRX::expand_address_with_checksum(&owner_u8_addr); - } - - let mut to_u8_addr: [u8; 21] = [0; 21]; - let mut to_address = String::from(""); - - if transfer_contract.to_address.len() >= TRX_ADD_RAW_LEN { - to_u8_addr.copy_from_slice(&transfer_contract.to_address[..]); - to_address = TRX::expand_address_with_checksum(&to_u8_addr); - } - - let value = U256::from_i64(transfer_contract.amount).to_f64(self.get_decimals()); - return Ok(TxInfo { - sender: owner_address, - receiver: to_address, - value, - tx_type: TxType::Transfer, - }); - } - - ContractType::TriggerSmartContract => { - let trigger_contract = - protocol::TriggerSmartContract::decode(parameter.value.as_slice())?; - let mut owner_u8_addr: [u8; 21] = [0; 21]; - let mut owner_address = String::from(""); - - if trigger_contract.owner_address.len() >= TRX_ADD_RAW_LEN { - owner_u8_addr.copy_from_slice(&trigger_contract.owner_address[..]); - owner_address = TRX::expand_address_with_checksum(&owner_u8_addr); - } - - let mut to_u8_addr: [u8; 21] = [0; 21]; - let mut to_address = String::from(""); - - if trigger_contract.contract_address.len() >= TRX_ADD_RAW_LEN { - to_u8_addr.copy_from_slice(&trigger_contract.contract_address[..]); - to_address = TRX::expand_address_with_checksum(&to_u8_addr); - } - - return Ok(TxInfo { - sender: owner_address, - receiver: to_address, - value: 0.0, - tx_type: TxType::TriggerContract, - }); - } - _ => {} - }; - - Ok(TxInfo { - sender: "".to_string(), - receiver: "".to_string(), - value: 0.0, - tx_type: TxType::Unknown, - }) + fn get_tx_info(&self, _raw_tx: Vec) -> Result { + Err(ChainError::NotSupported) } fn get_chain_type(&self) -> ChainType { @@ -288,9 +147,8 @@ impl Chain for TRX { #[cfg(test)] mod test { - use crate::chains::{Chain, Transaction}; + use crate::chains::Chain; use alloc::string::{String, ToString}; - use alloc::vec; #[test] fn test_trx_derive() { @@ -308,47 +166,6 @@ mod test { assert_eq!(addr, "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH"); } - #[test] - fn test_sign_tx() { - let hex_tx = hex::decode( - "0a02487c22080608af18\ - f6ec6c8340d8f8fae2e0315a65080112610a2d747970652e676f6f676c65\ - 617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e74\ - 7261637412300a1541e825d52582eec346c839b4875376117904a76cbc121\ - 54120ab1300cf70c048e4cf5d5b1b33f59653ed6626180a708fb1f7e2e031", - ) - .unwrap(); - - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let path = String::from("m/44'/195'/0'/0/0"); - - let seed = crate::chains::trx::TRX {} - .mnemonic_to_seed(mnemonic, String::from("")) - .unwrap(); - let pvk = crate::chains::trx::TRX {}.derive(seed, path).unwrap(); - assert_eq!(pvk.len(), 32); - - let tx = Transaction { - raw_data: hex_tx, - tx_hash: vec![], - signature: vec![], - options: None, - }; - let _tx = crate::chains::trx::TRX {}.sign_tx(pvk, tx).unwrap(); - } - - #[test] - fn test_decode_tx() { - let hex_tx = hex::decode("0a022986220894d3a7d6c869ebc840c0e1b2b5e1315a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a1541e825d52582eec346c839b4875376117904a76cbc12154120ab1300cf70c048e4cf5d5b1b33f59653ed6626180a70ae9cafb5e131").unwrap(); - - let info = crate::chains::trx::TRX {}.get_tx_info(hex_tx).unwrap(); - let res = tiny_json_rs::encode(info); - assert_eq!( - res, - r#"{"receiver":"TCwwZeH6so1X4R5kcdbKqa4GWuzF53xPqG","sender":"TX8h6Df74VpJsXF6sTDz1QJsq3Ec8dABc3","tx_type":"Transfer","value":0.00001}"# - ); - } - #[test] fn test_sign_message() { let mnemonic = diff --git a/packages/kos/src/chains/util.rs b/packages/kos/src/chains/util.rs index ad1fd63c..e658941c 100644 --- a/packages/kos/src/chains/util.rs +++ b/packages/kos/src/chains/util.rs @@ -22,3 +22,74 @@ pub fn hex_string_to_vec(hex: &str) -> Result, ChainError> { let hex = hex.trim_start_matches("0x"); hex::decode(hex).map_err(|_| ChainError::InvalidHex) } +pub fn byte_vectors_to_bytes(data: &Vec>) -> Vec { + let mut result = Vec::new(); + + let num_vecs = (data.len() as u32).to_le_bytes(); + result.extend_from_slice(&num_vecs); + + for vec in data { + let vec_len = (vec.len() as u32).to_le_bytes(); + result.extend_from_slice(&vec_len); + + result.extend_from_slice(vec); + } + + result +} +pub fn bytes_to_byte_vectors(bytes: Vec) -> Result>, ChainError> { + if bytes.len() < 4 { + return Err(ChainError::InvalidMessageSize); + } + + let mut result = Vec::new(); + let mut position = 0; + + let num_vecs = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize; + position += 4; + + for _ in 0..num_vecs { + if position + 4 > bytes.len() { + return Err(ChainError::InvalidMessageSize); + } + + let vec_len = u32::from_le_bytes([ + bytes[position], + bytes[position + 1], + bytes[position + 2], + bytes[position + 3], + ]) as usize; + position += 4; + + if position + vec_len > bytes.len() { + return Err(ChainError::InvalidMessageSize); + } + + let vec_data = bytes[position..position + vec_len].to_vec(); + result.push(vec_data); + position += vec_len; + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hex_string_to_vec() { + let hex_str = "0x1234567890abcdef"; + let result = hex_string_to_vec(hex_str).unwrap(); + assert_eq!(result, vec![0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef]); + } + + #[test] + fn test_hex_strings_to_bytes() { + let hex_strings = vec![hex::decode("db41e41de474e2cb6d997ae5aa5de9aa81512a19d1337881363a3c481431935992a118ba863b6d00612c638b5caf7bac65cb2cf31a7d30f9c5473fcb97bf620bc006bf0760963c13c1c1478adbc326b96338060f03487ebd1c3b261dbccd8daf").unwrap(), hex::decode("db41e41de8163a3c481431935992a118ba863b6d00612c638b5caf7bac65cb2c7ebd1c3b261dbccd8daf").unwrap()]; + let result = byte_vectors_to_bytes(&hex_strings); + + let decoded = bytes_to_byte_vectors(result).unwrap(); + assert_eq!(hex_strings, decoded); + } +} diff --git a/packages/kos/src/lib.rs b/packages/kos/src/lib.rs index 26056135..0828c6b9 100644 --- a/packages/kos/src/lib.rs +++ b/packages/kos/src/lib.rs @@ -1,9 +1,7 @@ #![cfg_attr(feature = "ksafe", no_std)] +extern crate alloc; pub mod chains; pub mod crypto; -pub mod protos; - -extern crate alloc; #[cfg(feature = "ksafe")] use core::alloc::{GlobalAlloc, Layout};