From 0053bef91e6e1463580c1f982744f9429d78efe7 Mon Sep 17 00:00:00 2001 From: pasta Date: Sun, 17 May 2026 15:33:34 -0500 Subject: [PATCH 1/4] fix(wasm-sdk): support binary grove path elements --- packages/js-evo-sdk/src/system/facade.ts | 11 +- .../tests/unit/facades/system.spec.ts | 14 +- packages/wasm-sdk/src/queries/system.rs | 996 ++++++++++++++++-- .../unit/conversion-simple-types.spec.ts | 63 ++ 4 files changed, 1011 insertions(+), 73 deletions(-) diff --git a/packages/js-evo-sdk/src/system/facade.ts b/packages/js-evo-sdk/src/system/facade.ts index 3877ec39a47..762d18c784b 100644 --- a/packages/js-evo-sdk/src/system/facade.ts +++ b/packages/js-evo-sdk/src/system/facade.ts @@ -41,13 +41,18 @@ export class SystemFacade { return w.getPrefundedSpecializedBalanceWithProofInfo(identityId); } - async pathElements(path: string[], keys: string[]): Promise { + async pathElements( + path: wasm.GrovePathSegment[], + keys: wasm.GrovePathSegment[], + ): Promise { const w = await this.sdk.getWasmSdkConnected(); return w.getPathElements(path, keys); } - async pathElementsWithProof(path: string[], keys: string[]): - Promise> { + async pathElementsWithProof( + path: wasm.GrovePathSegment[], + keys: wasm.GrovePathSegment[], + ): Promise> { const w = await this.sdk.getWasmSdkConnected(); return w.getPathElementsWithProofInfo(path, keys); } diff --git a/packages/js-evo-sdk/tests/unit/facades/system.spec.ts b/packages/js-evo-sdk/tests/unit/facades/system.spec.ts index 853de671cb5..58c0a92ab6a 100644 --- a/packages/js-evo-sdk/tests/unit/facades/system.spec.ts +++ b/packages/js-evo-sdk/tests/unit/facades/system.spec.ts @@ -76,15 +76,25 @@ describe('SystemFacade', () => { describe('pathElements()', () => { it('should forward to getPathElements', async () => { - await client.system.pathElements(['p'], ['k']); + const path = ['p', new Uint8Array([0x80, 0xff])]; + const keys = [new Uint8Array([0x01, 0x02])]; + + await client.system.pathElements(path, keys); + expect(getPathElementsStub).to.be.calledOnce(); + expect(getPathElementsStub).to.be.calledWithExactly(path, keys); }); }); describe('pathElementsWithProof()', () => { it('should forward to getPathElementsWithProofInfo', async () => { - await client.system.pathElementsWithProof(['p2'], ['k2']); + const path = ['p2', new Uint8Array([0x80, 0xff])]; + const keys = [new Uint8Array([0x03, 0x04])]; + + await client.system.pathElementsWithProof(path, keys); + expect(getPathElementsWithProofInfoStub).to.be.calledOnce(); + expect(getPathElementsWithProofInfoStub).to.be.calledWithExactly(path, keys); }); }); }); diff --git a/packages/wasm-sdk/src/queries/system.rs b/packages/wasm-sdk/src/queries/system.rs index 7bb6a6da84b..5ba5e1ba46c 100644 --- a/packages/wasm-sdk/src/queries/system.rs +++ b/packages/wasm-sdk/src/queries/system.rs @@ -3,14 +3,67 @@ use crate::impl_wasm_serde_conversions; use crate::queries::ProofMetadataResponseWasm; use crate::sdk::WasmSdk; use dash_sdk::dpp::core_types::validator_set::v0::ValidatorSetV0Getters; +use dash_sdk::drive::grovedb::{element::reference_path::path_from_reference_path_type, Element}; use dash_sdk::platform::Identifier; -use js_sys::{Array, BigInt}; +use js_sys::{Array, ArrayBuffer, BigInt, Object, Reflect, Uint8Array}; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use wasm_dpp2::identifier::{IdentifierLikeJs, IdentifierWasm}; use wasm_dpp2::ProTxHashWasm; +#[wasm_bindgen(typescript_custom_section)] +const GROVE_PATH_ELEMENT_TS: &'static str = r#" +export type GrovePathSegment = string | Uint8Array; + +export type GroveElementType = + | "item" + | "reference" + | "tree" + | "sumItem" + | "sumTree" + | "bigSumTree" + | "countTree" + | "countSumTree" + | "provableCountTree" + | "itemWithSumItem" + | "referenceWithSumItem" + | "provableCountSumTree" + | "provableSumTree" + | "commitmentTree" + | "mmrTree" + | "bulkAppendTree" + | "denseAppendOnlyFixedSizeTree" + | "nonCountedItem" + | "nonCountedReference" + | "nonCountedTree" + | "nonCountedSumItem" + | "nonCountedSumTree" + | "nonCountedBigSumTree" + | "nonCountedCountTree" + | "nonCountedCountSumTree" + | "nonCountedProvableCountTree" + | "nonCountedItemWithSumItem" + | "nonCountedReferenceWithSumItem" + | "nonCountedProvableCountSumTree" + | "nonCountedProvableSumTree" + | "nonCountedCommitmentTree" + | "nonCountedMmrTree" + | "nonCountedBulkAppendTree" + | "nonCountedDenseAppendOnlyFixedSizeTree" + | "notSummedSumTree" + | "notSummedBigSumTree" + | "notSummedCountSumTree" + | "notSummedProvableCountSumTree" + | "notSummedProvableSumTree" + | "notCountedOrSummedSumTree" + | "notCountedOrSummedBigSumTree" + | "notCountedOrSummedCountSumTree" + | "notCountedOrSummedProvableCountSumTree" + | "notCountedOrSummedProvableSumTree"; +"#; + #[wasm_bindgen(js_name = "StatusSoftware")] #[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -303,7 +356,6 @@ impl_wasm_serde_conversions!(StatusResponseWasm, StatusResponse); impl_wasm_serde_conversions!(QuorumInfoWasm, QuorumInfo); impl_wasm_serde_conversions!(CurrentQuorumsInfoWasm, CurrentQuorumsInfo); impl_wasm_serde_conversions!(PrefundedSpecializedBalanceWasm, PrefundedSpecializedBalance); -impl_wasm_serde_conversions!(PathElementWasm, PathElement); impl_wasm_serde_conversions!(StateTransitionResultWasm, StateTransitionResult); #[wasm_bindgen(js_name = "QuorumInfo")] @@ -408,18 +460,596 @@ impl PrefundedSpecializedBalanceWasm { #[serde(rename_all = "camelCase")] pub struct PathElementWasm { path: Vec, + #[serde(with = "path_element_bytes")] + key: Vec, + #[serde(with = "path_element_bytes_vec")] + path_bytes: Vec>, #[wasm_bindgen(getter_with_clone)] pub value: Option, + #[serde(with = "path_element_optional_bytes")] + value_bytes: Option>, + element_type: Option, + #[serde(with = "path_element_optional_i128")] + sum: Option, + #[serde(with = "path_element_optional_bytes_vec")] + reference_target: Option>>, + reference_target_error: Option, } impl PathElementWasm { - pub(crate) fn new(path: Vec, value: Option) -> Self { - Self { path, value } + fn missing(parent_path: &[Vec], key: &DecodedPathInput) -> Self { + let mut path_bytes = parent_path.to_vec(); + path_bytes.push(key.bytes.clone()); + + Self { + path: key.legacy_path_segment.clone().into_iter().collect(), + key: key.bytes.clone(), + path_bytes, + value: None, + value_bytes: None, + element_type: None, + sum: None, + reference_target: None, + reference_target_error: None, + } + } + + fn from_element(parent_path: &[Vec], key: &DecodedPathInput, element: &Element) -> Self { + let value_bytes = element_value_bytes(element); + let value = value_bytes.as_ref().map(|bytes| { + use base64::Engine; + base64::engine::general_purpose::STANDARD.encode(bytes) + }); + let mut path_bytes = parent_path.to_vec(); + path_bytes.push(key.bytes.clone()); + let (reference_target, reference_target_error) = + element_reference_target(element, parent_path, &key.bytes); + + Self { + path: key.legacy_path_segment.clone().into_iter().collect(), + key: key.bytes.clone(), + path_bytes, + value, + value_bytes, + element_type: Some(element_type_name(element).to_string()), + sum: element_sum(element), + reference_target, + reference_target_error, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct DecodedPathInput { + bytes: Vec, + legacy_path_segment: Option, +} + +enum PathInputValue<'a> { + Bytes(&'a [u8]), + String(&'a str), +} + +mod path_element_bytes { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Serialize, Deserialize)] + pub(super) struct BytesField( + #[serde(with = "dash_sdk::dpp::serialization::serde_bytes_var")] pub(super) Vec, + ); + + pub fn serialize(bytes: &[u8], serializer: S) -> Result + where + S: Serializer, + { + BytesField(bytes.to_owned()).serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + BytesField::deserialize(deserializer).map(|field| field.0) + } +} + +mod path_element_optional_bytes { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use super::path_element_bytes::BytesField; + + pub fn serialize(bytes: &Option>, serializer: S) -> Result + where + S: Serializer, + { + bytes + .as_ref() + .map(|bytes| BytesField(bytes.clone())) + .serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).map(|field| field.map(|bytes| bytes.0)) + } +} + +mod path_element_bytes_vec { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use super::path_element_bytes::BytesField; + + pub fn serialize(path: &[Vec], serializer: S) -> Result + where + S: Serializer, + { + path.iter() + .cloned() + .map(BytesField) + .collect::>() + .serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + Vec::::deserialize(deserializer) + .map(|path| path.into_iter().map(|bytes| bytes.0).collect()) + } +} + +mod path_element_optional_bytes_vec { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use super::path_element_bytes::BytesField; + + pub fn serialize(path: &Option>>, serializer: S) -> Result + where + S: Serializer, + { + path.as_ref() + .map(|path| path.iter().cloned().map(BytesField).collect::>()) + .serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>>, D::Error> + where + D: Deserializer<'de>, + { + Option::>::deserialize(deserializer) + .map(|path| path.map(|path| path.into_iter().map(|bytes| bytes.0).collect())) + } +} + +mod path_element_optional_i128 { + use serde::de::{self, Deserializer, Visitor}; + use serde::ser::Serializer; + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: Serializer, + { + match value { + Some(value) if serializer.is_human_readable() => { + serializer.serialize_some(&value.to_string()) + } + Some(value) => serializer.serialize_some(value), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + deserializer.deserialize_option(OptionI128Visitor) + } else { + serde::Deserialize::deserialize(deserializer) + } + } + + struct OptionI128Visitor; + + impl<'de> Visitor<'de> for OptionI128Visitor { + type Value = Option; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("null, an integer, or a string containing an i128") + } + + fn visit_none(self) -> Result { + Ok(None) + } + + fn visit_unit(self) -> Result { + Ok(None) + } + + fn visit_some>( + self, + deserializer: D, + ) -> Result { + deserializer.deserialize_any(I128Visitor).map(Some) + } + } + + struct I128Visitor; + + impl<'de> Visitor<'de> for I128Visitor { + type Value = i128; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an integer or a string containing an i128") + } + + fn visit_i64(self, value: i64) -> Result { + Ok(value as i128) + } + + fn visit_i128(self, value: i128) -> Result { + Ok(value) + } + + fn visit_u64(self, value: u64) -> Result { + Ok(value as i128) + } + + fn visit_u128(self, value: u128) -> Result { + i128::try_from(value) + .map_err(|_| de::Error::custom(format!("u128 value {value} out of i128 range"))) + } + + fn visit_str(self, value: &str) -> Result { + value + .parse::() + .map_err(|_| de::Error::custom(format!("invalid i128 string: {value}"))) + } + } +} + +/// Decode a public Grove path segment. +/// +/// String path segments preserve a legacy compatibility rule: decimal strings +/// in the `u8` range are decoded as a single byte, while other strings are +/// decoded as UTF-8. Use `Uint8Array` for unambiguous binary path segments. +fn decode_path_input(value: PathInputValue<'_>) -> DecodedPathInput { + match value { + PathInputValue::Bytes(bytes) => DecodedPathInput { + bytes: bytes.to_vec(), + legacy_path_segment: bytes_to_round_trippable_path_display(bytes), + }, + PathInputValue::String(value) => DecodedPathInput { + bytes: decode_path_string(value), + legacy_path_segment: Some(value.to_string()), + }, + } +} + +fn decode_key_input(value: PathInputValue<'_>) -> DecodedPathInput { + match value { + PathInputValue::Bytes(bytes) => DecodedPathInput { + bytes: bytes.to_vec(), + legacy_path_segment: bytes_to_round_trippable_key_display(bytes), + }, + PathInputValue::String(value) => DecodedPathInput { + bytes: value.as_bytes().to_vec(), + legacy_path_segment: Some(value.to_string()), + }, + } +} + +fn decode_path_string(value: &str) -> Vec { + if let Ok(number) = value.parse::() { + tracing::warn!( + "decoding Grove path string segment as legacy decimal u8; use Uint8Array for unambiguous binary paths" + ); + vec![number] + } else { + value.as_bytes().to_vec() + } +} + +fn decode_path_string_silent(value: &str) -> Vec { + value + .parse::() + .map(|number| vec![number]) + .unwrap_or_else(|_| value.as_bytes().to_vec()) +} + +fn decode_js_path_inputs( + array: &Array, + field: &str, +) -> Result, WasmSdkError> { + decode_js_inputs(array, field, decode_path_input) +} + +fn decode_js_key_inputs(array: &Array, field: &str) -> Result, WasmSdkError> { + decode_js_inputs(array, field, decode_key_input) +} + +fn decode_js_inputs( + array: &Array, + field: &str, + decode: fn(PathInputValue<'_>) -> DecodedPathInput, +) -> Result, WasmSdkError> { + array + .iter() + .enumerate() + .map(|(index, value)| { + if is_uint8_array_value(&value) { + let bytes = Uint8Array::new(&value).to_vec(); + Ok(decode(PathInputValue::Bytes(&bytes))) + } else if let Some(string) = value.as_string() { + Ok(decode(PathInputValue::String(&string))) + } else { + Err(WasmSdkError::invalid_argument(format!( + "{}[{}] must be a string or Uint8Array", + field, index + ))) + } + }) + .collect() +} + +fn is_uint8_array_value(value: &JsValue) -> bool { + if value.is_instance_of::() { + return true; + } + + if !ArrayBuffer::is_view(value) { + return false; + } + + Reflect::get(value, &JsValue::from_str("constructor")) + .ok() + .and_then(|constructor| Reflect::get(&constructor, &JsValue::from_str("name")).ok()) + .and_then(|name| name.as_string()) + .as_deref() + == Some("Uint8Array") +} + +fn decoded_bytes(inputs: &[DecodedPathInput]) -> Vec> { + inputs.iter().map(|input| input.bytes.clone()).collect() +} + +fn bytes_to_round_trippable_path_display(bytes: &[u8]) -> Option { + if let Ok(value) = std::str::from_utf8(bytes) { + if value + .bytes() + .all(|byte| byte.is_ascii_graphic() || byte == b' ') + && decode_path_string_silent(value) == bytes + { + return Some(value.to_string()); + } + } + + None +} + +fn bytes_to_round_trippable_key_display(bytes: &[u8]) -> Option { + if let Ok(value) = std::str::from_utf8(bytes) { + if value + .bytes() + .all(|byte| byte.is_ascii_graphic() || byte == b' ') + { + return Some(value.to_string()); + } + } + + None +} + +fn bytes_path_to_js_array(path: &[Vec]) -> Array { + let array = Array::new(); + for segment in path { + array.push(&Uint8Array::from(segment.as_slice())); + } + array +} + +fn set_js_property(object: &Object, property: &str, value: &JsValue) -> Result<(), WasmSdkError> { + Reflect::set(object, &JsValue::from_str(property), value).map_err(|_| { + WasmSdkError::generic(format!("failed to set PathElement.{} property", property)) + })?; + Ok(()) +} + +fn element_value_bytes(element: &Element) -> Option> { + match element { + Element::Item(bytes, _) => Some(bytes.clone()), + Element::ItemWithSumItem(bytes, _, _) => Some(bytes.clone()), + Element::NonCounted(inner) + | Element::NotSummed(inner) + | Element::NotCountedOrSummed(inner) => element_value_bytes(inner), + _ => None, + } +} + +fn element_sum(element: &Element) -> Option { + match element { + Element::SumItem(sum, _) => Some(*sum as i128), + Element::SumTree(_, sum, _) => Some(*sum as i128), + Element::BigSumTree(_, sum, _) => Some(*sum), + Element::CountSumTree(_, _, sum, _) => Some(*sum as i128), + Element::ItemWithSumItem(_, sum, _) => Some(*sum as i128), + Element::ProvableCountSumTree(_, _, sum, _) => Some(*sum as i128), + Element::ReferenceWithSumItem(_, _, sum, _) => Some(*sum as i128), + Element::ProvableSumTree(_, sum, _) => Some(*sum as i128), + Element::NonCounted(inner) + | Element::NotSummed(inner) + | Element::NotCountedOrSummed(inner) => element_sum(inner), + _ => None, + } +} + +fn element_reference_target( + element: &Element, + parent_path: &[Vec], + key: &[u8], +) -> (Option>>, Option) { + match element { + Element::Reference(reference_path, _, _) + | Element::ReferenceWithSumItem(reference_path, _, _, _) => { + match path_from_reference_path_type(reference_path.clone(), parent_path, Some(key)) { + Ok(target) => (Some(target), None), + Err(error) => { + let message = error.to_string(); + tracing::warn!("failed to resolve GroveDB reference target: {}", message); + (None, Some(message)) + } + } + } + Element::NonCounted(inner) + | Element::NotSummed(inner) + | Element::NotCountedOrSummed(inner) => element_reference_target(inner, parent_path, key), + _ => (None, None), + } +} + +fn element_type_name(element: &Element) -> &'static str { + match element { + Element::Item(_, _) => "item", + Element::Reference(_, _, _) => "reference", + Element::Tree(_, _) => "tree", + Element::SumItem(_, _) => "sumItem", + Element::SumTree(_, _, _) => "sumTree", + Element::BigSumTree(_, _, _) => "bigSumTree", + Element::CountTree(_, _, _) => "countTree", + Element::CountSumTree(_, _, _, _) => "countSumTree", + Element::ProvableCountTree(_, _, _) => "provableCountTree", + Element::ItemWithSumItem(_, _, _) => "itemWithSumItem", + Element::ReferenceWithSumItem(_, _, _, _) => "referenceWithSumItem", + Element::ProvableCountSumTree(_, _, _, _) => "provableCountSumTree", + Element::ProvableSumTree(_, _, _) => "provableSumTree", + Element::CommitmentTree(_, _, _) => "commitmentTree", + Element::MmrTree(_, _) => "mmrTree", + Element::BulkAppendTree(_, _, _) => "bulkAppendTree", + Element::DenseAppendOnlyFixedSizeTree(_, _, _) => "denseAppendOnlyFixedSizeTree", + Element::NonCounted(inner) => non_counted_element_type_name(inner), + Element::NotSummed(inner) => not_summed_element_type_name(inner), + Element::NotCountedOrSummed(inner) => not_counted_or_summed_element_type_name(inner), + } +} + +fn non_counted_element_type_name(element: &Element) -> &'static str { + match element { + Element::Item(_, _) => "nonCountedItem", + Element::Reference(_, _, _) => "nonCountedReference", + Element::Tree(_, _) => "nonCountedTree", + Element::SumItem(_, _) => "nonCountedSumItem", + Element::SumTree(_, _, _) => "nonCountedSumTree", + Element::BigSumTree(_, _, _) => "nonCountedBigSumTree", + Element::CountTree(_, _, _) => "nonCountedCountTree", + Element::CountSumTree(_, _, _, _) => "nonCountedCountSumTree", + Element::ProvableCountTree(_, _, _) => "nonCountedProvableCountTree", + Element::ItemWithSumItem(_, _, _) => "nonCountedItemWithSumItem", + Element::ReferenceWithSumItem(_, _, _, _) => "nonCountedReferenceWithSumItem", + Element::ProvableCountSumTree(_, _, _, _) => "nonCountedProvableCountSumTree", + Element::ProvableSumTree(_, _, _) => "nonCountedProvableSumTree", + Element::CommitmentTree(_, _, _) => "nonCountedCommitmentTree", + Element::MmrTree(_, _) => "nonCountedMmrTree", + Element::BulkAppendTree(_, _, _) => "nonCountedBulkAppendTree", + Element::DenseAppendOnlyFixedSizeTree(_, _, _) => "nonCountedDenseAppendOnlyFixedSizeTree", + Element::NonCounted(_) | Element::NotSummed(_) | Element::NotCountedOrSummed(_) => { + element_type_name(element) + } + } +} + +fn not_summed_element_type_name(element: &Element) -> &'static str { + match element { + Element::SumTree(_, _, _) => "notSummedSumTree", + Element::BigSumTree(_, _, _) => "notSummedBigSumTree", + Element::CountSumTree(_, _, _, _) => "notSummedCountSumTree", + Element::ProvableCountSumTree(_, _, _, _) => "notSummedProvableCountSumTree", + Element::ProvableSumTree(_, _, _) => "notSummedProvableSumTree", + Element::NonCounted(_) | Element::NotSummed(_) | Element::NotCountedOrSummed(_) => { + element_type_name(element) + } + _ => element_type_name(element), + } +} + +fn not_counted_or_summed_element_type_name(element: &Element) -> &'static str { + match element { + Element::SumTree(_, _, _) => "notCountedOrSummedSumTree", + Element::BigSumTree(_, _, _) => "notCountedOrSummedBigSumTree", + Element::CountSumTree(_, _, _, _) => "notCountedOrSummedCountSumTree", + Element::ProvableCountSumTree(_, _, _, _) => "notCountedOrSummedProvableCountSumTree", + Element::ProvableSumTree(_, _, _) => "notCountedOrSummedProvableSumTree", + Element::NonCounted(_) | Element::NotSummed(_) | Element::NotCountedOrSummed(_) => { + element_type_name(element) + } + _ => element_type_name(element), } } #[wasm_bindgen(js_class = PathElement)] impl PathElementWasm { + #[wasm_bindgen(js_name = toObject)] + pub fn to_object(&self) -> Result { + let object = Object::new(); + + set_js_property(&object, "path", &self.path().into())?; + set_js_property(&object, "key", &self.key().into())?; + set_js_property(&object, "pathBytes", &self.path_bytes().into())?; + + let value = self + .value + .as_ref() + .map(|value| JsValue::from_str(value)) + .unwrap_or(JsValue::UNDEFINED); + set_js_property(&object, "value", &value)?; + + let value_bytes = self + .value_bytes() + .map(JsValue::from) + .unwrap_or(JsValue::UNDEFINED); + set_js_property(&object, "valueBytes", &value_bytes)?; + + let element_type = self + .element_type() + .map(|element_type| JsValue::from_str(&element_type)) + .unwrap_or(JsValue::UNDEFINED); + set_js_property(&object, "elementType", &element_type)?; + + let sum = self.sum().map(JsValue::from).unwrap_or(JsValue::UNDEFINED); + set_js_property(&object, "sum", &sum)?; + + let reference_target = self + .reference_target() + .map(JsValue::from) + .unwrap_or(JsValue::UNDEFINED); + set_js_property(&object, "referenceTarget", &reference_target)?; + + let reference_target_error = self + .reference_target_error() + .map(|error| JsValue::from_str(&error)) + .unwrap_or(JsValue::UNDEFINED); + set_js_property(&object, "referenceTargetError", &reference_target_error)?; + + Ok(object.into()) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object(obj: Object) -> Result { + wasm_dpp2::serialization::from_object(obj.into()).map_err(WasmSdkError::from) + } + + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> Result { + wasm_dpp2::serialization::to_json(self).map_err(WasmSdkError::from) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(js: Object) -> Result { + wasm_dpp2::serialization::from_json(js.into()).map_err(WasmSdkError::from) + } + #[wasm_bindgen(getter)] pub fn path(&self) -> Array { let array = Array::new(); @@ -428,6 +1058,53 @@ impl PathElementWasm { } array } + + #[wasm_bindgen(getter)] + pub fn key(&self) -> Uint8Array { + Uint8Array::from(self.key.as_slice()) + } + + #[wasm_bindgen(getter = "pathBytes", unchecked_return_type = "Uint8Array[]")] + pub fn path_bytes(&self) -> Array { + bytes_path_to_js_array(&self.path_bytes) + } + + #[wasm_bindgen(getter = "valueBytes")] + pub fn value_bytes(&self) -> Option { + self.value_bytes + .as_ref() + .map(|bytes| Uint8Array::from(bytes.as_slice())) + } + + #[wasm_bindgen( + getter = "elementType", + unchecked_return_type = "GroveElementType | undefined" + )] + pub fn element_type(&self) -> Option { + self.element_type.clone() + } + + #[wasm_bindgen(getter)] + pub fn sum(&self) -> Option { + self.sum + .as_ref() + .and_then(|sum| BigInt::new(&JsValue::from_str(&sum.to_string())).ok()) + } + + #[wasm_bindgen( + getter = "referenceTarget", + unchecked_return_type = "Uint8Array[] | undefined" + )] + pub fn reference_target(&self) -> Option { + self.reference_target + .as_ref() + .map(|path| bytes_path_to_js_array(path)) + } + + #[wasm_bindgen(getter = "referenceTargetError")] + pub fn reference_target_error(&self) -> Option { + self.reference_target_error.clone() + } } #[wasm_bindgen(js_name = "StateTransitionResult")] @@ -880,34 +1557,20 @@ impl WasmSdk { )] pub async fn get_path_elements( &self, - path: Vec, - keys: Vec, + #[wasm_bindgen(unchecked_param_type = "GrovePathSegment[]")] path: Array, + #[wasm_bindgen(unchecked_param_type = "GrovePathSegment[]")] keys: Array, ) -> Result { - use dash_sdk::drive::grovedb::Element; use dash_sdk::platform::FetchMany; use drive_proof_verifier::types::{Elements, KeysInPath}; - // Convert string path to byte vectors - // Path elements can be either numeric values (like "96" for Balances) or string keys - let path_bytes: Vec> = path - .iter() - .map(|p| { - // Try to parse as a u8 number first (for root tree paths) - if let Ok(num) = p.parse::() { - vec![num] - } else { - // Otherwise treat as a string key - p.as_bytes().to_vec() - } - }) - .collect(); - - // Convert string keys to byte vectors - let key_bytes: Vec> = keys.iter().map(|k| k.as_bytes().to_vec()).collect(); + let decoded_path = decode_js_path_inputs(&path, "path")?; + let decoded_keys = decode_js_key_inputs(&keys, "keys")?; + let path_bytes = decoded_bytes(&decoded_path); + let key_bytes = decoded_bytes(&decoded_keys); // Create the query let query = KeysInPath { - path: path_bytes, + path: path_bytes.clone(), keys: key_bytes, }; @@ -916,18 +1579,14 @@ impl WasmSdk { // Convert the result to our response format let elements_array = Array::new(); - for key in keys { - let value = path_elements_result - .get(key.as_bytes()) + for key in &decoded_keys { + let path_element = path_elements_result + .get(key.bytes.as_slice()) .and_then(|element_opt| element_opt.as_ref()) - .and_then(|element| { - element.as_item_bytes().ok().map(|bytes| { - use base64::Engine; - base64::engine::general_purpose::STANDARD.encode(bytes) - }) - }); - - elements_array.push(&JsValue::from(PathElementWasm::new(vec![key], value))); + .map(|element| PathElementWasm::from_element(&path_bytes, key, element)) + .unwrap_or_else(|| PathElementWasm::missing(&path_bytes, key)); + + elements_array.push(&JsValue::from(path_element)); } Ok(elements_array) @@ -1003,34 +1662,20 @@ impl WasmSdk { )] pub async fn get_path_elements_with_proof_info( &self, - path: Vec, - keys: Vec, + #[wasm_bindgen(unchecked_param_type = "GrovePathSegment[]")] path: Array, + #[wasm_bindgen(unchecked_param_type = "GrovePathSegment[]")] keys: Array, ) -> Result { - use dash_sdk::drive::grovedb::Element; use dash_sdk::platform::FetchMany; use drive_proof_verifier::types::KeysInPath; - // Convert string path to byte vectors - // Path elements can be either numeric values (like "96" for Balances) or string keys - let path_bytes: Vec> = path - .iter() - .map(|p| { - // Try to parse as a u8 number first (for root tree paths) - if let Ok(num) = p.parse::() { - vec![num] - } else { - // Otherwise treat as a string key - p.as_bytes().to_vec() - } - }) - .collect(); - - // Convert string keys to byte vectors - let key_bytes: Vec> = keys.iter().map(|k| k.as_bytes().to_vec()).collect(); + let decoded_path = decode_js_path_inputs(&path, "path")?; + let decoded_keys = decode_js_key_inputs(&keys, "keys")?; + let path_bytes = decoded_bytes(&decoded_path); + let key_bytes = decoded_bytes(&decoded_keys); // Create the query let query = KeysInPath { - path: path_bytes, + path: path_bytes.clone(), keys: key_bytes, }; @@ -1039,18 +1684,14 @@ impl WasmSdk { Element::fetch_many_with_metadata_and_proof(self.as_ref(), query, None).await?; let elements_array = Array::new(); - for key in keys { - let value = path_elements_result - .get(key.as_bytes()) + for key in &decoded_keys { + let path_element = path_elements_result + .get(key.bytes.as_slice()) .and_then(|element_opt| element_opt.as_ref()) - .and_then(|element| { - element.as_item_bytes().ok().map(|bytes| { - use base64::Engine; - base64::engine::general_purpose::STANDARD.encode(bytes) - }) - }); - - elements_array.push(&JsValue::from(PathElementWasm::new(vec![key], value))); + .map(|element| PathElementWasm::from_element(&path_bytes, key, element)) + .unwrap_or_else(|| PathElementWasm::missing(&path_bytes, key)); + + elements_array.push(&JsValue::from(path_element)); } Ok(ProofMetadataResponseWasm::from_sdk_parts( @@ -1060,3 +1701,222 @@ impl WasmSdk { )) } } + +#[cfg(test)] +mod tests { + use super::*; + use dash_sdk::drive::grovedb::element::reference_path::ReferencePathType; + + #[test] + fn should_decode_raw_bytes_without_utf8_expansion() { + let input = [0x80, 0xff]; + + let path = decode_path_input(PathInputValue::Bytes(&input)); + let key = decode_key_input(PathInputValue::Bytes(&input)); + + assert_eq!(path.bytes, input); + assert_eq!(key.bytes, input); + assert_eq!(path.legacy_path_segment, None); + assert_eq!(key.legacy_path_segment, None); + } + + #[test] + fn should_decode_path_decimal_string_as_single_byte() { + let path = decode_path_input(PathInputValue::String("96")); + + assert_eq!(path.bytes, vec![96]); + } + + #[test] + fn should_decode_key_string_as_utf8() { + let key = decode_key_input(PathInputValue::String("96")); + + assert_eq!(key.bytes, vec![0x39, 0x36]); + assert_eq!(key.legacy_path_segment.as_deref(), Some("96")); + } + + #[test] + fn should_preserve_utf8_numeric_key_bytes_in_legacy_path_segment() { + let key = decode_key_input(PathInputValue::Bytes(b"96")); + + assert_eq!(key.bytes, vec![0x39, 0x36]); + assert_eq!(key.legacy_path_segment.as_deref(), Some("96")); + } + + #[test] + fn should_convert_item_element_with_compatible_value_and_bytes() { + let parent_path = vec![vec![1]]; + let key = decode_key_input(PathInputValue::String("key")); + let element = Element::Item(b"value".to_vec(), None); + + let path_element = PathElementWasm::from_element(&parent_path, &key, &element); + + assert_eq!(path_element.value.as_deref(), Some("dmFsdWU=")); + assert_eq!(path_element.value_bytes, Some(b"value".to_vec())); + assert_eq!(path_element.element_type.as_deref(), Some("item")); + assert_eq!(path_element.key, b"key".to_vec()); + assert_eq!(path_element.path_bytes, vec![vec![1], b"key".to_vec()]); + } + + #[test] + fn should_convert_tree_elements_without_value_bytes() { + let parent_path = vec![vec![1]]; + let key = decode_key_input(PathInputValue::String("subtree")); + let tree = Element::Tree(None, None); + let sum_tree = Element::SumTree(None, 42, None); + + let tree_element = PathElementWasm::from_element(&parent_path, &key, &tree); + let sum_tree_element = PathElementWasm::from_element(&parent_path, &key, &sum_tree); + + assert_eq!(tree_element.value, None); + assert_eq!(tree_element.value_bytes, None); + assert_eq!(tree_element.element_type.as_deref(), Some("tree")); + assert_eq!(sum_tree_element.value, None); + assert_eq!(sum_tree_element.value_bytes, None); + assert_eq!(sum_tree_element.element_type.as_deref(), Some("sumTree")); + assert_eq!(sum_tree_element.sum, Some(42)); + } + + #[test] + fn should_convert_sum_item_and_reference_metadata() { + let parent_path = vec![b"parent".to_vec()]; + let key = decode_key_input(PathInputValue::String("key")); + let sum_item = Element::SumItem(-5, None); + let reference = Element::Reference( + ReferencePathType::SiblingReference(b"other".to_vec()), + None, + None, + ); + let reference_with_sum = Element::ReferenceWithSumItem( + ReferencePathType::SiblingReference(b"weighted".to_vec()), + None, + 7, + None, + ); + let provable_sum_tree = Element::ProvableSumTree(None, 9, None); + let not_counted_or_summed = + Element::NotCountedOrSummed(Box::new(Element::ProvableSumTree(None, 11, None))); + + let sum_element = PathElementWasm::from_element(&parent_path, &key, &sum_item); + let reference_element = PathElementWasm::from_element(&parent_path, &key, &reference); + let reference_with_sum_element = + PathElementWasm::from_element(&parent_path, &key, &reference_with_sum); + let provable_sum_tree_element = + PathElementWasm::from_element(&parent_path, &key, &provable_sum_tree); + let not_counted_or_summed_element = + PathElementWasm::from_element(&parent_path, &key, ¬_counted_or_summed); + + assert_eq!(sum_element.element_type.as_deref(), Some("sumItem")); + assert_eq!(sum_element.sum, Some(-5)); + assert_eq!(reference_element.element_type.as_deref(), Some("reference")); + assert_eq!( + reference_element.reference_target, + Some(vec![b"parent".to_vec(), b"other".to_vec()]) + ); + assert_eq!(reference_element.reference_target_error, None); + assert_eq!( + reference_with_sum_element.element_type.as_deref(), + Some("referenceWithSumItem") + ); + assert_eq!(reference_with_sum_element.sum, Some(7)); + assert_eq!( + reference_with_sum_element.reference_target, + Some(vec![b"parent".to_vec(), b"weighted".to_vec()]) + ); + assert_eq!( + provable_sum_tree_element.element_type.as_deref(), + Some("provableSumTree") + ); + assert_eq!(provable_sum_tree_element.sum, Some(9)); + assert_eq!( + not_counted_or_summed_element.element_type.as_deref(), + Some("notCountedOrSummedProvableSumTree") + ); + assert_eq!(not_counted_or_summed_element.sum, Some(11)); + } + + #[test] + fn should_report_reference_resolution_errors() { + let parent_path = vec![b"parent".to_vec()]; + let key = decode_key_input(PathInputValue::String("key")); + let invalid_reference = Element::Reference( + ReferencePathType::UpstreamRootHeightReference(2, vec![b"target".to_vec()]), + None, + None, + ); + + let reference_element = + PathElementWasm::from_element(&parent_path, &key, &invalid_reference); + + assert_eq!(reference_element.element_type.as_deref(), Some("reference")); + assert_eq!(reference_element.reference_target, None); + assert!(reference_element.reference_target_error.is_some()); + } + + #[test] + fn should_map_supported_grovedb_element_types() { + let reference_path = || ReferencePathType::SiblingReference(b"target".to_vec()); + let cases = vec![ + (Element::Item(vec![1], None), "item"), + ( + Element::Reference(reference_path(), None, None), + "reference", + ), + (Element::Tree(None, None), "tree"), + (Element::SumItem(1, None), "sumItem"), + (Element::SumTree(None, 1, None), "sumTree"), + (Element::BigSumTree(None, 1, None), "bigSumTree"), + (Element::CountTree(None, 1, None), "countTree"), + (Element::CountSumTree(None, 1, 2, None), "countSumTree"), + ( + Element::ProvableCountTree(None, 1, None), + "provableCountTree", + ), + ( + Element::ItemWithSumItem(vec![1], 2, None), + "itemWithSumItem", + ), + ( + Element::ReferenceWithSumItem(reference_path(), None, 2, None), + "referenceWithSumItem", + ), + ( + Element::ProvableCountSumTree(None, 1, 2, None), + "provableCountSumTree", + ), + (Element::ProvableSumTree(None, 2, None), "provableSumTree"), + (Element::CommitmentTree(1, 2, None), "commitmentTree"), + (Element::MmrTree(1, None), "mmrTree"), + (Element::BulkAppendTree(1, 2, None), "bulkAppendTree"), + ( + Element::DenseAppendOnlyFixedSizeTree(1, 2, None), + "denseAppendOnlyFixedSizeTree", + ), + ( + Element::NonCounted(Box::new(Element::ReferenceWithSumItem( + reference_path(), + None, + 2, + None, + ))), + "nonCountedReferenceWithSumItem", + ), + ( + Element::NonCounted(Box::new(Element::ProvableSumTree(None, 2, None))), + "nonCountedProvableSumTree", + ), + ( + Element::NotSummed(Box::new(Element::ProvableSumTree(None, 2, None))), + "notSummedProvableSumTree", + ), + ( + Element::NotCountedOrSummed(Box::new(Element::ProvableSumTree(None, 2, None))), + "notCountedOrSummedProvableSumTree", + ), + ]; + + for (element, expected_type) in cases { + assert_eq!(element_type_name(&element), expected_type); + } + } +} diff --git a/packages/wasm-sdk/tests/unit/conversion-simple-types.spec.ts b/packages/wasm-sdk/tests/unit/conversion-simple-types.spec.ts index 6df185ee47a..e3fb1eb7521 100644 --- a/packages/wasm-sdk/tests/unit/conversion-simple-types.spec.ts +++ b/packages/wasm-sdk/tests/unit/conversion-simple-types.spec.ts @@ -292,6 +292,69 @@ describe('Simple Type Conversions', () => { }); }); + describe('PathElement', () => { + const jsonFixture = { + path: ['key'], + key: 'AQI=', + pathBytes: ['AwQ=', 'AQI='], + value: 'dmFsdWU=', + valueBytes: 'dmFsdWU=', + elementType: 'sumItem', + sum: '9007199254740993', + referenceTarget: ['BQY='], + referenceTargetError: null, + }; + + const objectInputFixture = { + path: ['key'], + key: new Uint8Array([1, 2]), + pathBytes: [new Uint8Array([3, 4]), new Uint8Array([1, 2])], + value: 'dmFsdWU=', + valueBytes: new Uint8Array([118, 97, 108, 117, 101]), + elementType: 'sumItem', + sum: 9007199254740993n, + referenceTarget: [new Uint8Array([5, 6])], + referenceTargetError: null, + }; + + const objectOutputFixture = { + ...objectInputFixture, + referenceTargetError: undefined, + }; + + describe('toJSON()', () => { + it('should serialize binary fields as base64 strings', () => { + const result = sdk.PathElement.fromJSON(jsonFixture); + expect(result.toJSON()).to.deep.equal(jsonFixture); + }); + }); + + describe('toObject()', () => { + it('should serialize binary fields as Uint8Array and sum as BigInt', () => { + const result = sdk.PathElement.fromObject(objectInputFixture); + expect(result.toObject()).to.deep.equal(objectOutputFixture); + }); + }); + + describe('fromObject()', () => { + it('should expose typed getters for tree exploration fields', () => { + const result = sdk.PathElement.fromObject(objectInputFixture); + + expect(result.key).to.deep.equal(new Uint8Array([1, 2])); + expect(result.pathBytes).to.deep.equal([ + new Uint8Array([3, 4]), + new Uint8Array([1, 2]), + ]); + expect(result.value).to.equal('dmFsdWU='); + expect(result.valueBytes).to.deep.equal(new Uint8Array([118, 97, 108, 117, 101])); + expect(result.elementType).to.equal('sumItem'); + expect(result.sum).to.equal(9007199254740993n); + expect(result.referenceTarget).to.deep.equal([new Uint8Array([5, 6])]); + expect(result.referenceTargetError).to.be.undefined(); + }); + }); + }); + describe('TokenPriceInfo', () => { const testId = 'H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1'; From 3d11ae7afa06a43bbc2d2fcbb01e975dad0c6ce3 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 18 May 2026 21:13:44 -0500 Subject: [PATCH 2/4] fix(wasm-sdk): allow non-ASCII UTF-8 in path/key legacy display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy-display fallback for binary grove path inputs was gated on every byte being ASCII-graphic-or-space, which dropped perfectly valid round-trip cases. For keys, decode_key_input(String) just calls value.as_bytes(), so any valid UTF-8 trivially round-trips; for paths, the existing decode_path_string_silent equality check is the real correctness guarantee. Loosen both filters so callers that switch from string to Uint8Array inputs like "café" still get the human-readable legacy path segment instead of an empty path. Addresses CodeRabbit review on PR #3657. --- packages/wasm-sdk/src/queries/system.rs | 50 ++++++++++++++----------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/wasm-sdk/src/queries/system.rs b/packages/wasm-sdk/src/queries/system.rs index 5ba5e1ba46c..168f97d4d7e 100644 --- a/packages/wasm-sdk/src/queries/system.rs +++ b/packages/wasm-sdk/src/queries/system.rs @@ -817,30 +817,12 @@ fn decoded_bytes(inputs: &[DecodedPathInput]) -> Vec> { } fn bytes_to_round_trippable_path_display(bytes: &[u8]) -> Option { - if let Ok(value) = std::str::from_utf8(bytes) { - if value - .bytes() - .all(|byte| byte.is_ascii_graphic() || byte == b' ') - && decode_path_string_silent(value) == bytes - { - return Some(value.to_string()); - } - } - - None + let value = std::str::from_utf8(bytes).ok()?; + (decode_path_string_silent(value) == bytes).then(|| value.to_string()) } fn bytes_to_round_trippable_key_display(bytes: &[u8]) -> Option { - if let Ok(value) = std::str::from_utf8(bytes) { - if value - .bytes() - .all(|byte| byte.is_ascii_graphic() || byte == b' ') - { - return Some(value.to_string()); - } - } - - None + std::str::from_utf8(bytes).ok().map(str::to_string) } fn bytes_path_to_js_array(path: &[Vec]) -> Array { @@ -1743,6 +1725,32 @@ mod tests { assert_eq!(key.legacy_path_segment.as_deref(), Some("96")); } + #[test] + fn should_preserve_non_ascii_utf8_key_bytes_in_legacy_path_segment() { + let bytes = "café".as_bytes(); + let key = decode_key_input(PathInputValue::Bytes(bytes)); + + assert_eq!(key.bytes, bytes); + assert_eq!(key.legacy_path_segment.as_deref(), Some("café")); + } + + #[test] + fn should_preserve_non_ascii_utf8_path_bytes_in_legacy_path_segment() { + let bytes = "café".as_bytes(); + let path = decode_path_input(PathInputValue::Bytes(bytes)); + + assert_eq!(path.bytes, bytes); + assert_eq!(path.legacy_path_segment.as_deref(), Some("café")); + } + + #[test] + fn should_drop_legacy_path_segment_for_ambiguous_numeric_path_bytes() { + let path = decode_path_input(PathInputValue::Bytes(b"96")); + + assert_eq!(path.bytes, b"96".to_vec()); + assert_eq!(path.legacy_path_segment, None); + } + #[test] fn should_convert_item_element_with_compatible_value_and_bytes() { let parent_path = vec![vec![1]]; From ec22be7a51108eca3d1ebfd03bf55433f825dc2d Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 19 May 2026 19:07:14 -0500 Subject: [PATCH 3/4] refactor(wasm-sdk): address grove path element review nits - Accept cross-realm Node Buffer in is_uint8_array_value by also matching constructor.name === "Buffer". - Skip the redundant JS-side Uint8Array allocation when decoding path/key segments by reinterpreting the already-verified JsValue via unchecked_ref. - Drop the dead wrapper arm in not_summed_/not_counted_or_summed_element_type_name; the catch-all already dispatches wrappers correctly through element_type_name. - Surface BigInt construction failures in the sum getter via expect() instead of silently producing undefined, so a future invariant violation is visible. Addresses CodeRabbit review nits on PR #3657. --- packages/wasm-sdk/src/queries/system.rs | 26 ++++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/wasm-sdk/src/queries/system.rs b/packages/wasm-sdk/src/queries/system.rs index 168f97d4d7e..9bfe0ead42c 100644 --- a/packages/wasm-sdk/src/queries/system.rs +++ b/packages/wasm-sdk/src/queries/system.rs @@ -781,7 +781,7 @@ fn decode_js_inputs( .enumerate() .map(|(index, value)| { if is_uint8_array_value(&value) { - let bytes = Uint8Array::new(&value).to_vec(); + let bytes = value.unchecked_ref::().to_vec(); Ok(decode(PathInputValue::Bytes(&bytes))) } else if let Some(string) = value.as_string() { Ok(decode(PathInputValue::String(&string))) @@ -804,12 +804,15 @@ fn is_uint8_array_value(value: &JsValue) -> bool { return false; } - Reflect::get(value, &JsValue::from_str("constructor")) + let constructor_name = Reflect::get(value, &JsValue::from_str("constructor")) .ok() .and_then(|constructor| Reflect::get(&constructor, &JsValue::from_str("name")).ok()) - .and_then(|name| name.as_string()) - .as_deref() - == Some("Uint8Array") + .and_then(|name| name.as_string()); + + matches!( + constructor_name.as_deref(), + Some("Uint8Array") | Some("Buffer") + ) } fn decoded_bytes(inputs: &[DecodedPathInput]) -> Vec> { @@ -949,9 +952,6 @@ fn not_summed_element_type_name(element: &Element) -> &'static str { Element::CountSumTree(_, _, _, _) => "notSummedCountSumTree", Element::ProvableCountSumTree(_, _, _, _) => "notSummedProvableCountSumTree", Element::ProvableSumTree(_, _, _) => "notSummedProvableSumTree", - Element::NonCounted(_) | Element::NotSummed(_) | Element::NotCountedOrSummed(_) => { - element_type_name(element) - } _ => element_type_name(element), } } @@ -963,9 +963,6 @@ fn not_counted_or_summed_element_type_name(element: &Element) -> &'static str { Element::CountSumTree(_, _, _, _) => "notCountedOrSummedCountSumTree", Element::ProvableCountSumTree(_, _, _, _) => "notCountedOrSummedProvableCountSumTree", Element::ProvableSumTree(_, _, _) => "notCountedOrSummedProvableSumTree", - Element::NonCounted(_) | Element::NotSummed(_) | Element::NotCountedOrSummed(_) => { - element_type_name(element) - } _ => element_type_name(element), } } @@ -1068,9 +1065,10 @@ impl PathElementWasm { #[wasm_bindgen(getter)] pub fn sum(&self) -> Option { - self.sum - .as_ref() - .and_then(|sum| BigInt::new(&JsValue::from_str(&sum.to_string())).ok()) + self.sum.as_ref().map(|sum| { + BigInt::new(&JsValue::from_str(&sum.to_string())) + .expect("i128 decimal string always parses as BigInt") + }) } #[wasm_bindgen( From 04bd8f488a0859ce0f90d27debd09bdc9e5274db Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 20 May 2026 16:09:06 -0500 Subject: [PATCH 4/4] fix(wasm-sdk): handle ProvableCountProvableSumTree grove element Latest v3.1-dev brought in a new grovedb Element variant ProvableCountProvableSumTree. Add it to element_sum (it carries a sum) and to every element_type_name dispatcher, plus the GroveElementType TS union, so PathElement keeps reporting an element type instead of failing exhaustiveness checks. --- packages/wasm-sdk/src/queries/system.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/wasm-sdk/src/queries/system.rs b/packages/wasm-sdk/src/queries/system.rs index 9bfe0ead42c..a8939115c7f 100644 --- a/packages/wasm-sdk/src/queries/system.rs +++ b/packages/wasm-sdk/src/queries/system.rs @@ -30,6 +30,7 @@ export type GroveElementType = | "itemWithSumItem" | "referenceWithSumItem" | "provableCountSumTree" + | "provableCountProvableSumTree" | "provableSumTree" | "commitmentTree" | "mmrTree" @@ -47,6 +48,7 @@ export type GroveElementType = | "nonCountedItemWithSumItem" | "nonCountedReferenceWithSumItem" | "nonCountedProvableCountSumTree" + | "nonCountedProvableCountProvableSumTree" | "nonCountedProvableSumTree" | "nonCountedCommitmentTree" | "nonCountedMmrTree" @@ -56,11 +58,13 @@ export type GroveElementType = | "notSummedBigSumTree" | "notSummedCountSumTree" | "notSummedProvableCountSumTree" + | "notSummedProvableCountProvableSumTree" | "notSummedProvableSumTree" | "notCountedOrSummedSumTree" | "notCountedOrSummedBigSumTree" | "notCountedOrSummedCountSumTree" | "notCountedOrSummedProvableCountSumTree" + | "notCountedOrSummedProvableCountProvableSumTree" | "notCountedOrSummedProvableSumTree"; "#; @@ -862,6 +866,7 @@ fn element_sum(element: &Element) -> Option { Element::CountSumTree(_, _, sum, _) => Some(*sum as i128), Element::ItemWithSumItem(_, sum, _) => Some(*sum as i128), Element::ProvableCountSumTree(_, _, sum, _) => Some(*sum as i128), + Element::ProvableCountProvableSumTree(_, _, sum, _) => Some(*sum as i128), Element::ReferenceWithSumItem(_, _, sum, _) => Some(*sum as i128), Element::ProvableSumTree(_, sum, _) => Some(*sum as i128), Element::NonCounted(inner) @@ -909,6 +914,7 @@ fn element_type_name(element: &Element) -> &'static str { Element::ItemWithSumItem(_, _, _) => "itemWithSumItem", Element::ReferenceWithSumItem(_, _, _, _) => "referenceWithSumItem", Element::ProvableCountSumTree(_, _, _, _) => "provableCountSumTree", + Element::ProvableCountProvableSumTree(_, _, _, _) => "provableCountProvableSumTree", Element::ProvableSumTree(_, _, _) => "provableSumTree", Element::CommitmentTree(_, _, _) => "commitmentTree", Element::MmrTree(_, _) => "mmrTree", @@ -934,6 +940,9 @@ fn non_counted_element_type_name(element: &Element) -> &'static str { Element::ItemWithSumItem(_, _, _) => "nonCountedItemWithSumItem", Element::ReferenceWithSumItem(_, _, _, _) => "nonCountedReferenceWithSumItem", Element::ProvableCountSumTree(_, _, _, _) => "nonCountedProvableCountSumTree", + Element::ProvableCountProvableSumTree(_, _, _, _) => { + "nonCountedProvableCountProvableSumTree" + } Element::ProvableSumTree(_, _, _) => "nonCountedProvableSumTree", Element::CommitmentTree(_, _, _) => "nonCountedCommitmentTree", Element::MmrTree(_, _) => "nonCountedMmrTree", @@ -951,6 +960,9 @@ fn not_summed_element_type_name(element: &Element) -> &'static str { Element::BigSumTree(_, _, _) => "notSummedBigSumTree", Element::CountSumTree(_, _, _, _) => "notSummedCountSumTree", Element::ProvableCountSumTree(_, _, _, _) => "notSummedProvableCountSumTree", + Element::ProvableCountProvableSumTree(_, _, _, _) => { + "notSummedProvableCountProvableSumTree" + } Element::ProvableSumTree(_, _, _) => "notSummedProvableSumTree", _ => element_type_name(element), } @@ -962,6 +974,9 @@ fn not_counted_or_summed_element_type_name(element: &Element) -> &'static str { Element::BigSumTree(_, _, _) => "notCountedOrSummedBigSumTree", Element::CountSumTree(_, _, _, _) => "notCountedOrSummedCountSumTree", Element::ProvableCountSumTree(_, _, _, _) => "notCountedOrSummedProvableCountSumTree", + Element::ProvableCountProvableSumTree(_, _, _, _) => { + "notCountedOrSummedProvableCountProvableSumTree" + } Element::ProvableSumTree(_, _, _) => "notCountedOrSummedProvableSumTree", _ => element_type_name(element), }