Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Multiple WASM-SDK improvements #102

Open
wants to merge 19 commits into
base: gamma-dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions wallet/core/src/storage/transaction/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<bool> {
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.
Expand Down
50 changes: 50 additions & 0 deletions wallet/core/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ pub fn try_kaspa_str_to_sompi<S: Into<String>>(s: S) -> Result<Option<u64>> {
Ok(Some(str_to_sompi(amount)?))
}

pub fn try_kaspa_str_to_unit<S: Into<String>>(s: S, decimals: u32) -> Result<Option<u64>> {
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: Into<String>>(s: S) -> Result<Option<i64>> {
let s: String = s.into();
let amount = s.trim();
Expand All @@ -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)))
Expand Down Expand Up @@ -108,3 +130,31 @@ fn str_to_sompi(amount: &str) -> Result<u64> {
};
Ok(integer + decimal)
}

fn str_to_unit(amount: &str, decimals: u32) -> Result<u64> {
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::<u64>()? * sompi_per_unit);
};

// Parse the integer part of the amount
let integer = amount[..dot_idx].parse::<u64>()? * 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::<u64>()? * 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::<u64>()?
};
Ok(integer + decimal)
}
20 changes: 20 additions & 0 deletions wallet/core/src/wasm/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ pub fn kaspa_to_sompi(kaspa: String) -> Option<BigInt> {
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<BigInt> {
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.
///
Expand All @@ -31,6 +40,17 @@ pub fn sompi_to_kaspa_string(sompi: ISompiToKaspa) -> Result<String> {
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<String> {
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,
Expand Down
7 changes: 7 additions & 0 deletions wallet/core/src/wasm/utxo/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions wallet/keys/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 13 additions & 7 deletions wallet/keys/src/privatekey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,6 +40,11 @@ impl PrivateKey {
pub fn try_new(key: &str) -> Result<PrivateKey> {
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 {
Expand All @@ -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<Keypair, JsError> {
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions wallet/keys/src/publickey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

use crate::imports::*;

use kaspa_addresses::AddressT;
use kaspa_consensus_core::network::NetworkType;
use ripemd::{Digest, Ripemd160};
use sha2::Sha256;
Expand Down Expand Up @@ -236,8 +237,9 @@ impl XOnlyPublicKey {
}

#[wasm_bindgen(js_name = fromAddress)]
pub fn from_address(address: &Address) -> Result<XOnlyPublicKey> {
Ok(secp256k1::XOnlyPublicKey::from_slice(&address.payload)?.into())
pub fn from_address(address: &AddressT) -> Result<XOnlyPublicKey> {
let address = Address::try_cast_from(address)?;
Ok(secp256k1::XOnlyPublicKey::from_slice(&address.as_ref().payload)?.into())
}
}

Expand Down