Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/js-evo-sdk/src/identities/facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ export class IdentitiesFacade {
return w.getIdentityUnproved(identityId);
}

async getKeys(query: wasm.IdentityKeysQuery): Promise<wasm.IdentityKeyInfo[]> {
async getKeys(query: wasm.IdentityKeysQuery): Promise<wasm.IdentityPublicKey[]> {
const w = await this.sdk.getWasmSdkConnected();
return w.getIdentityKeys(query);
}

async getKeysWithProof(query: wasm.IdentityKeysQuery): Promise<wasm.ProofMetadataResponseTyped<wasm.IdentityKeyInfo[]>> {
async getKeysWithProof(query: wasm.IdentityKeysQuery): Promise<wasm.ProofMetadataResponseTyped<wasm.IdentityPublicKey[]>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getIdentityKeysWithProofInfo(query);
}
Expand Down
30 changes: 27 additions & 3 deletions packages/wasm-dpp2/src/core/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen(typescript_custom_section)]
const NETWORK_LIKE_TS: &'static str = r#"
/**
* Flexible network type that accepts Network enum or string names.
* Flexible network type that accepts Network enum, string names, or numeric values.
*
* String values (case-insensitive): "mainnet", "testnet", "devnet", "regtest"
* Numeric values: 0 (mainnet), 1 (testnet), 2 (devnet), 3 (regtest)
*/
export type NetworkLike = Network | "mainnet" | "testnet" | "devnet" | "regtest";
export type NetworkLike = Network | "mainnet" | "testnet" | "devnet" | "regtest" | 0 | 1 | 2 | 3;
"#;

#[wasm_bindgen(js_name = "Network")]
Expand All @@ -27,6 +28,7 @@ pub enum NetworkWasm {
impl TryFrom<JsValue> for NetworkWasm {
type Error = WasmDppError;
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
// Handle string input
if let Some(enum_val) = value.as_string() {
return match enum_val.to_lowercase().as_str() {
"mainnet" => Ok(NetworkWasm::Mainnet),
Expand All @@ -40,8 +42,30 @@ impl TryFrom<JsValue> for NetworkWasm {
};
}

// Handle numeric enum value (Network.Mainnet = 0, Testnet = 1, etc.)
if let Some(num) = value.as_f64() {
// Validate that the number is a non-negative integer within u32 range
if num.fract() != 0.0 || num < 0.0 || num > u32::MAX as f64 {
return Err(WasmDppError::invalid_argument(format!(
"network value must be a non-negative integer, got '{}'",
num
)));
}

return match num as u32 {
0 => Ok(NetworkWasm::Mainnet),
1 => Ok(NetworkWasm::Testnet),
2 => Ok(NetworkWasm::Devnet),
3 => Ok(NetworkWasm::Regtest),
_ => Err(WasmDppError::invalid_argument(format!(
"unsupported network value '{}'. Expected: 0 (mainnet), 1 (testnet), 2 (devnet), or 3 (regtest)",
num
))),
};
}
Comment thread
shumkov marked this conversation as resolved.

Err(WasmDppError::invalid_argument(
"network must be a string: 'mainnet', 'testnet', 'devnet', or 'regtest'",
"network must be a string ('mainnet', 'testnet', 'devnet', 'regtest') or Network enum value",
))
}
}
Expand Down
35 changes: 31 additions & 4 deletions packages/wasm-dpp2/src/platform_address/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use dpp::address_funds::PlatformAddress;
use dpp::dashcore::Network;
use js_sys::Uint8Array;
use serde::de::{self, Error, Visitor};
use serde::{Deserialize, Deserializer};
use serde::ser::Serializer;
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;
use wasm_bindgen::prelude::*;

Expand Down Expand Up @@ -103,9 +104,20 @@ impl TryFrom<&str> for PlatformAddressWasm {
type Error = WasmDppError;

fn try_from(value: &str) -> Result<Self, Self::Error> {
// Try parsing as bech32m string
PlatformAddress::from_bech32m_string(value)
.map(|(addr, _network)| PlatformAddressWasm(addr))
// Try parsing as bech32m string first (e.g., "dashevo1..." or "tdashevo1...")
if let Ok((addr, _network)) = PlatformAddress::from_bech32m_string(value) {
return Ok(PlatformAddressWasm(addr));
}

// Fall back to hex decoding for compatibility with serialized format
let bytes = hex::decode(value).map_err(|e| {
WasmDppError::invalid_argument(format!(
"Invalid PlatformAddress: not valid bech32m or hex: {}",
e
))
})?;
PlatformAddress::from_bytes(&bytes)
.map(PlatformAddressWasm)
.map_err(|e| WasmDppError::invalid_argument(e.to_string()))
}
}
Expand Down Expand Up @@ -165,6 +177,21 @@ impl<'de> Deserialize<'de> for PlatformAddressWasm {
}
}

impl Serialize for PlatformAddressWasm {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if serializer.is_human_readable() {
// JSON, TOML, etc. - use hex string
serializer.serialize_str(&hex::encode(self.0.to_bytes()))
} else {
// Binary formats (bincode, MessagePack, etc.) - use raw bytes
serializer.serialize_bytes(&self.0.to_bytes())
}
}
}
Comment thread
shumkov marked this conversation as resolved.

#[wasm_bindgen(js_class = PlatformAddress)]
impl PlatformAddressWasm {
#[wasm_bindgen(getter = __type)]
Expand Down
155 changes: 142 additions & 13 deletions packages/wasm-dpp2/src/serialization/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ use serde_json::Value as JsonValue;
use wasm_bindgen::JsValue;
use wasm_bindgen::prelude::*;

/// Try to call toJSON() on a WASM object if it has one.
///
/// Returns Some(result) if the object has a toJSON method and it succeeds,
/// None otherwise (for plain objects, arrays, primitives, etc.)
fn try_call_to_json(value: &JsValue) -> Option<JsValue> {
if !value.is_object() || value.is_null() || js_sys::Array::is_array(value) {
return None;
}

// Check for toJSON method
let to_json_fn = js_sys::Reflect::get(value, &JsValue::from_str("toJSON")).ok()?;
if !to_json_fn.is_function() {
return None;
}

let func: js_sys::Function = to_json_fn.unchecked_into();
func.call0(value).ok()
}

/// Convert JsValue to serde_json::Value, handling BigInt values and WASM objects.
///
/// This function:
Expand All @@ -37,18 +56,9 @@ use wasm_bindgen::prelude::*;
/// - Falls back to serde_wasm_bindgen conversion for plain objects
pub fn js_value_to_json(value: &JsValue) -> WasmDppResult<JsonValue> {
// Check if the value has a toJSON method (WASM objects like DataContractWasm, IdentityWasm)
if value.is_object()
&& !value.is_null()
&& !js_sys::Array::is_array(value)
&& let Ok(to_json_fn) = js_sys::Reflect::get(value, &JsValue::from_str("toJSON"))
&& to_json_fn.is_function()
{
let func: js_sys::Function = to_json_fn.unchecked_into();
// Call toJSON() on the object
if let Ok(json_result) = func.call0(value) {
// Recursively convert the result (it might contain BigInt or nested WASM objects)
return js_value_to_json(&json_result);
}
if let Some(json_result) = try_call_to_json(value) {
// Recursively convert the result (it might contain BigInt or nested WASM objects)
return js_value_to_json(&json_result);
}

let normalized = normalize_js_value_for_json(value)?;
Expand All @@ -70,12 +80,15 @@ pub fn json_to_js_value(value: &JsonValue) -> WasmDppResult<JsValue> {
/// Recursively normalizes a JsValue for JSON conversion.
///
/// This converts:
/// - WASM objects with toJSON() method - calls toJSON() and normalizes the result
/// - BigInt values to strings (JSON doesn't support BigInt natively)
/// - Uint8Array to plain arrays (so they serialize as JSON number arrays)
/// - JavaScript Map to plain objects
/// - Recursively processes nested objects and arrays
///
/// Performance: Uses fast path for primitives, only recursively processes objects/arrays.
fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult<JsValue> {
// Fast path: primitives that can't contain BigInt
// Fast path: primitives that can't contain BigInt or need conversion
if value.is_string()
|| value.as_f64().is_some()
|| value.is_null()
Expand All @@ -85,6 +98,7 @@ fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult<JsValue> {
return Ok(value.clone());
}

// Convert BigInt to string (JSON doesn't support BigInt)
if value.is_bigint() {
let bigint: js_sys::BigInt = value.clone().unchecked_into();
let bigint_str = bigint
Expand All @@ -101,6 +115,13 @@ fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult<JsValue> {
return Ok(plain_array.into());
}

// Convert JavaScript Map to an object for JSON compatibility
// Maps don't have enumerable properties, so Object.keys() returns empty array
if value.is_instance_of::<js_sys::Map>() {
return normalize_map_for_json(value);
}

// Handle arrays - recursively normalize each element
if js_sys::Array::is_array(value) {
let arr = js_sys::Array::from(value);
let new_arr = js_sys::Array::new();
Expand All @@ -112,7 +133,15 @@ fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult<JsValue> {
return Ok(new_arr.into());
}

// Handle objects - check for toJSON method first (WASM objects), then normalize properties
if value.is_object() && !value.is_null() {
// Try to call toJSON() on WASM objects (Identity, Identifier, DataContract, etc.)
if let Some(json_result) = try_call_to_json(value) {
// Recursively normalize the result (might contain BigInt, nested objects, etc.)
return normalize_js_value_for_json(&json_result);
}

// Plain object - normalize each property
let obj = Object::from(value.clone());
let new_obj = Object::new();
let keys = Object::keys(&obj);
Expand All @@ -135,6 +164,88 @@ fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult<JsValue> {
Ok(value.clone())
}

/// Convert a JavaScript Map key to a string for JSON object keys.
fn map_key_to_string(key: &JsValue) -> String {
// String keys - use as-is
if let Some(s) = key.as_string() {
return s;
}

// Number keys - convert to string
if key.as_f64().is_some() {
return js_sys::Number::from(key.clone())
.to_string(10)
.map(|s| s.into())
.unwrap_or_else(|_| "0".to_string());
}

// BigInt keys - convert to string
if key.is_bigint() {
let bigint: js_sys::BigInt = key.clone().unchecked_into();
return bigint
.to_string(10)
.map(|s| s.into())
.unwrap_or_else(|_| "0".to_string());
}

// Objects with toString (like Identifier) - call toString()
if let Ok(to_string_fn) = js_sys::Reflect::get(key, &JsValue::from_str("toString"))
&& to_string_fn.is_function()
{
let func: js_sys::Function = to_string_fn.unchecked_into();
if let Ok(str_result) = func.call0(key)
&& let Some(s) = str_result.as_string()
{
return s;
}
}

// Fallback - use debug representation
format!("{:?}", key)
}

/// Convert a JavaScript Map to a plain object for JSON serialization.
fn normalize_map_for_json(value: &JsValue) -> WasmDppResult<JsValue> {
let map: js_sys::Map = value.clone().unchecked_into();
let new_obj = Object::new();

// We need to collect errors from the closure since for_each doesn't support Result
let error: std::cell::RefCell<Option<WasmDppError>> = std::cell::RefCell::new(None);

map.for_each(&mut |val, key| {
// Skip if we already have an error
if error.borrow().is_some() {
return;
}

let key_str = map_key_to_string(&key);

// Normalize the value - handle WASM objects, BigInt, nested Maps, etc.
match normalize_js_value_for_json(&val) {
Ok(normalized_val) => {
if let Err(e) =
js_sys::Reflect::set(&new_obj, &JsValue::from_str(&key_str), &normalized_val)
{
*error.borrow_mut() = Some(WasmDppError::serialization(format!(
"Failed to set Map entry '{}': {:?}",
key_str, e
)));
}
}
Err(e) => {
*error.borrow_mut() = Some(e);
}
}
});

// Check if any error occurred during iteration
if let Some(e) = error.into_inner() {
return Err(e);
}

Ok(new_obj.into())
}

/// Serialize to JsValue as a JS object (non-human-readable).
///
/// Uses the serde-wasm-bindgen serializer with `is_human_readable() -> false`,
Expand Down Expand Up @@ -354,6 +465,24 @@ pub fn js_value_to_platform_value(value: &JsValue) -> WasmDppResult<platform_val
))
}

/// Test helper: Convert a JsValue to a JSON-compatible JsValue.
///
/// This function is exposed for testing purposes to validate the Map normalization
/// and BigInt handling logic in unit tests. It calls `js_value_to_json` internally
/// and converts the result back to a JsValue.
///
/// # Example (JavaScript)
/// ```javascript
/// const map = new Map([['key', 'value']]);
/// const json = testJsValueToJson(map);
/// console.log(json); // { key: 'value' }
/// ```
#[wasm_bindgen(js_name = testJsValueToJson)]
pub fn test_js_value_to_json(value: &JsValue) -> Result<JsValue, WasmDppError> {
let json_value = js_value_to_json(value)?;
json_to_js_value(&json_value)
}

/// Macro to implement `toObject`, `fromObject`, `toJSON`, and `fromJSON` methods
/// for a wasm_bindgen newtype wrapper using the serialization::conversions module.
///
Expand Down
24 changes: 12 additions & 12 deletions packages/wasm-dpp2/tests/unit/BatchTransition.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,23 @@ describe('BatchTransition', () => {
});

it('should allow to convert batch transition to base64 and back', () => {
const documentInstance = new wasm.Document(document, documentTypeName, revision, dataContractId, ownerId, id);
const createTransition = new wasm.DocumentCreateTransition(documentInstance, BigInt(1));
const documentInstance = new wasm.Document(document, documentTypeName, revision, dataContractId, ownerId, id);
const createTransition = new wasm.DocumentCreateTransition(documentInstance, BigInt(1));

const documentTransition = createTransition.toDocumentTransition();
const documentTransition = createTransition.toDocumentTransition();

const batchedTransition = new wasm.BatchedTransition(documentTransition);
const batchedTransition = new wasm.BatchedTransition(documentTransition);

const batch = wasm.BatchTransition.fromBatchedTransitions([batchedTransition], documentInstance.ownerId, 1);
const batch = wasm.BatchTransition.fromBatchedTransitions([batchedTransition], documentInstance.ownerId, 1);

const base64 = batch.toBase64();
const bytes = batch.toBytes();
const base64 = batch.toBase64();
const bytes = batch.toBytes();

expect(Buffer.from(base64, 'base64')).to.deep.equal(Buffer.from(bytes));
expect(Buffer.from(base64, 'base64')).to.deep.equal(Buffer.from(bytes));

const restoredBatch = wasm.BatchTransition.fromBase64(base64);
const restoredBatch = wasm.BatchTransition.fromBase64(base64);

expect(Buffer.from(restoredBatch.toBytes())).to.deep.equal(Buffer.from(bytes));
expect(Buffer.from(restoredBatch.toBytes())).to.deep.equal(Buffer.from(bytes));
});

it('should round-trip via object and JSON', () => {
Expand All @@ -87,8 +87,8 @@ describe('BatchTransition', () => {
const fromJson = wasm.BatchTransition.fromJSON(json);
expect(Buffer.from(fromJson.toBytes())).to.deep.equal(Buffer.from(batch.toBytes()));
});
});
describe('tokens', () => {
});
describe('tokens', () => {
it('should allow to create from v1 transition', () => {
const baseTransition = new wasm.TokenBaseTransition(BigInt(1), 1, dataContractId, ownerId);

Expand Down
4 changes: 3 additions & 1 deletion packages/wasm-dpp2/tests/unit/DataContract.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import getWasm from './helpers/wasm.js';
import { json, object, id, ownerId } from './mocks/DataContract/index.js';
import {
json, object, id, ownerId
} from './mocks/DataContract/index.js';
import { fromHexString } from './utils/hex.js';

let wasm;
Expand Down
Loading
Loading