diff --git a/wallet/core/src/storage/transaction/record.rs b/wallet/core/src/storage/transaction/record.rs index 05be3b69f2..65266fcae0 100644 --- a/wallet/core/src/storage/transaction/record.rs +++ b/wallet/core/src/storage/transaction/record.rs @@ -6,6 +6,7 @@ use super::*; use crate::imports::*; use crate::storage::{Binding, BindingT}; use crate::tx::PendingTransactionInner; +use kaspa_addresses::AddressT; use workflow_core::time::{unixtime_as_millis_u64, unixtime_to_locale_string}; use workflow_wasm::utils::try_get_js_value_prop; @@ -814,8 +815,15 @@ impl TransactionRecord { /// Check if the transaction record has the given address within the associated UTXO set. #[wasm_bindgen(js_name = hasAddress)] - pub fn has_address(&self, address: &Address) -> bool { - self.transaction_data.has_address(address) + pub fn has_address(&self, address: &AddressT) -> Result { + let address = Address::try_cast_from(address)?; + Ok(self.transaction_data.has_address(address.as_ref())) + } + + /// Check if the transaction record is coinbase sourced. + #[wasm_bindgen(getter, js_name = "isCoinbase")] + pub fn is_coinbase_sourced(&self) -> bool { + self.is_coinbase() } /// Serialize the transaction record to a JavaScript object. diff --git a/wallet/core/src/utils.rs b/wallet/core/src/utils.rs index c732e3f68d..a5890d047a 100644 --- a/wallet/core/src/utils.rs +++ b/wallet/core/src/utils.rs @@ -19,6 +19,16 @@ pub fn try_kaspa_str_to_sompi>(s: S) -> Result> { Ok(Some(str_to_sompi(amount)?)) } +pub fn try_kaspa_str_to_unit>(s: S, decimals: u32) -> Result> { + let s: String = s.into(); + let amount = s.trim(); + if amount.is_empty() { + return Ok(None); + } + + Ok(Some(str_to_unit(amount, decimals)?)) +} + pub fn try_kaspa_str_to_sompi_i64>(s: S) -> Result> { let s: String = s.into(); let amount = s.trim(); @@ -45,6 +55,18 @@ pub fn sompi_to_kaspa_string(sompi: u64) -> String { sompi_to_kaspa(sompi).separated_string() } +#[inline] +pub fn sompi_to_unit(sompi: u64, decimals: u32) -> f64 { + let sompi_per_unit = 10u64.pow(decimals); + + sompi as f64 / sompi_per_unit as f64 +} + +#[inline] +pub fn sompi_to_unit_string(sompi: u64, decimals: u32) -> String { + sompi_to_unit(sompi, decimals).separated_string() +} + #[inline] pub fn sompi_to_kaspa_string_with_trailing_zeroes(sompi: u64) -> String { separated_float!(format!("{:.8}", sompi_to_kaspa(sompi))) @@ -108,3 +130,31 @@ fn str_to_sompi(amount: &str) -> Result { }; Ok(integer + decimal) } + +fn str_to_unit(amount: &str, decimals: u32) -> Result { + let sompi_per_unit = 10u64.pow(decimals); + + // Check if the amount contains a decimal point, if doesn't return value directly. + let Some(dot_idx) = amount.find('.') else { + return Ok(amount.parse::()? * sompi_per_unit); + }; + + // Parse the integer part of the amount + let integer = amount[..dot_idx].parse::()? * sompi_per_unit; + + let decimal = &amount[dot_idx + 1..]; + let decimal_len = decimal.len(); + let decimal = if decimal_len == 0 { + // If there are no digits after the decimal point, the fractional value is 0. + 0 + } else if decimal_len <= decimals as usize { + // If its within allowed decimals range, parse it as u64 and pad with zeros to the right to reach the correct precision. + decimal.parse::()? * 10u64.pow(decimals - decimal_len as u32) + } else { + // Truncate values longer than allowed decimal places. + // TODO - discuss how to handle values longer than supplied decimal places. + // (reject, truncate, ceil(), etc.) + decimal[..decimals as usize].parse::()? + }; + Ok(integer + decimal) +} diff --git a/wallet/core/src/wasm/utils.rs b/wallet/core/src/wasm/utils.rs index a06c6136a3..c5cad8ac88 100644 --- a/wallet/core/src/wasm/utils.rs +++ b/wallet/core/src/wasm/utils.rs @@ -20,6 +20,15 @@ pub fn kaspa_to_sompi(kaspa: String) -> Option { crate::utils::try_kaspa_str_to_sompi(kaspa).ok().flatten().map(Into::into) } +/// Convert a Kaspa string to a specific unit represented by bigint. +/// This function provides correct precision handling and +/// can be used to parse user input. +/// @category Wallet SDK +#[wasm_bindgen(js_name = "kaspaToUnit")] +pub fn kaspa_to_unit(kaspa: String, decimals: u32) -> Option { + crate::utils::try_kaspa_str_to_unit(kaspa, decimals).ok().flatten().map(Into::into) +} + /// /// Convert Sompi to a string representation of the amount in Kaspa. /// @@ -31,6 +40,17 @@ pub fn sompi_to_kaspa_string(sompi: ISompiToKaspa) -> Result { Ok(crate::utils::sompi_to_kaspa_string(sompi)) } +/// +/// Convert Sompi to a string representation of an unit in Kaspa. +/// +/// @category Wallet SDK +/// +#[wasm_bindgen(js_name = "sompiToUnitString")] +pub fn sompi_to_unit_string(sompi: ISompiToKaspa, decimals: u32) -> Result { + let sompi = sompi.try_as_u64()?; + Ok(crate::utils::sompi_to_unit_string(sompi, decimals)) +} + /// /// Format a Sompi amount to a string representation of the amount in Kaspa with a suffix /// based on the network type (e.g. `KAS` for mainnet, `TKAS` for testnet, diff --git a/wallet/core/src/wasm/utxo/context.rs b/wallet/core/src/wasm/utxo/context.rs index 3298a4829e..6fd7b37846 100644 --- a/wallet/core/src/wasm/utxo/context.rs +++ b/wallet/core/src/wasm/utxo/context.rs @@ -8,6 +8,7 @@ use kaspa_addresses::AddressOrStringArrayT; use kaspa_consensus_client::UtxoEntryReferenceArrayT; use kaspa_hashes::Hash; use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; +use kaspa_wasm_core::types::HexString; declare! { IUtxoContextArgs, @@ -147,6 +148,12 @@ impl UtxoContext { self.inner().clear().await } + /// Deterministic ID of the context, allows differentiation across event notifications. + #[wasm_bindgen(getter, js_name = "id")] + pub fn id(&self) -> HexString { + self.inner.id().to_hex().into() + } + #[wasm_bindgen(getter, js_name = "isActive")] pub fn active(&self) -> bool { let processor = self.inner().processor(); diff --git a/wallet/keys/src/error.rs b/wallet/keys/src/error.rs index 0059a09420..d599d6c1e8 100644 --- a/wallet/keys/src/error.rs +++ b/wallet/keys/src/error.rs @@ -66,6 +66,9 @@ pub enum Error { #[error("Invalid UTF-8 sequence")] Utf8(#[from] std::str::Utf8Error), + + #[error(transparent)] + AddressError(#[from] kaspa_addresses::AddressError), } impl Error { diff --git a/wallet/keys/src/privatekey.rs b/wallet/keys/src/privatekey.rs index 554bdf36e3..3f83052c1a 100644 --- a/wallet/keys/src/privatekey.rs +++ b/wallet/keys/src/privatekey.rs @@ -5,6 +5,7 @@ use crate::imports::*; use crate::keypair::Keypair; use js_sys::{Array, Uint8Array}; +use rand::thread_rng; /// Data structure that envelops a Private Key. /// @category Wallet SDK @@ -39,6 +40,11 @@ impl PrivateKey { pub fn try_new(key: &str) -> Result { Ok(Self { inner: secp256k1::SecretKey::from_str(key)? }) } + + #[wasm_bindgen(js_name = random)] + pub fn create_new() -> PrivateKey { + Self { inner: secp256k1::SecretKey::new(&mut thread_rng()) } + } } impl PrivateKey { @@ -49,13 +55,6 @@ impl PrivateKey { #[wasm_bindgen] impl PrivateKey { - /// Returns the [`PrivateKey`] key encoded as a hex string. - #[wasm_bindgen(js_name = toString)] - pub fn to_hex(&self) -> String { - use kaspa_utils::hex::ToHex; - self.secret_bytes().to_vec().to_hex() - } - /// Generate a [`Keypair`] from this [`PrivateKey`]. #[wasm_bindgen(js_name = toKeypair)] pub fn to_keypair(&self) -> Result { @@ -91,6 +90,13 @@ impl PrivateKey { let address = Address::new(network.try_into()?, AddressVersion::PubKeyECDSA, &payload); Ok(address) } + + /// Returns the [`PrivateKey`] key encoded as a hex string. + #[wasm_bindgen(js_name = toString)] + pub fn to_hex(&self) -> String { + use kaspa_utils::hex::ToHex; + self.secret_bytes().to_vec().to_hex() + } } impl TryCastFromJs for PrivateKey { diff --git a/wallet/keys/src/publickey.rs b/wallet/keys/src/publickey.rs index 235eb80804..423969238f 100644 --- a/wallet/keys/src/publickey.rs +++ b/wallet/keys/src/publickey.rs @@ -19,6 +19,7 @@ use crate::imports::*; +use kaspa_addresses::AddressT; use kaspa_consensus_core::network::NetworkType; use ripemd::{Digest, Ripemd160}; use sha2::Sha256; @@ -236,8 +237,9 @@ impl XOnlyPublicKey { } #[wasm_bindgen(js_name = fromAddress)] - pub fn from_address(address: &Address) -> Result { - Ok(secp256k1::XOnlyPublicKey::from_slice(&address.payload)?.into()) + pub fn from_address(address: &AddressT) -> Result { + let address = Address::try_cast_from(address)?; + Ok(secp256k1::XOnlyPublicKey::from_slice(&address.as_ref().payload)?.into()) } }