Skip to content

Conversation

@shumkov
Copy link
Collaborator

@shumkov shumkov commented Jan 16, 2026

Issue being fixed or feature implemented

Standardizes the wasm-dpp2 API with proper TypeScript types, improves developer experience with better error messages

What was done?

API Standardization (wasm-dpp2)

  • TypeScript Types: Added proper TypeScript type definitions for all extern types (e.g., IdentifierLike, IdentifierLikeOrUndefined, NetworkLike, PlatformVersionLike)
  • Typed Parameters: Replaced JsValue with specific typed parameters in method signatures
  • Typed Return Objects: Added dedicated types for JSON and plain object returns (e.g., IdentityJson, DocumentJson)
  • Consistent Naming: Renamed parameters from snake_case to camelCase, removed get_ prefix from getters
  • BigInt for u64: Strictly use BigInt for all u64 values to prevent precision loss
  • Conversion Methods: Added toJSON(), toObject(), fromJSON(), fromObject() methods consistently across types

Utility Functions (wasm-dpp2/wasm-sdk)

  • Added try_from_options, try_from_options_optional, try_from_options_with for safe property extraction
  • Added try_to_u64, try_to_u32, try_to_u16, try_to_u8 with string parsing and NaN/Infinity validation
  • Added try_to_string, try_to_bytes, try_to_array, try_to_vec, try_to_map, try_to_object
  • Added impl_try_from_js_value! and impl_wasm_type_info! macros
  • Added JsMapExt trait for DRY Map building patterns
  • Improved error messages with field name context

Bug Fixes (wasm-dpp2)

  • Fixed from_bytes functions panicking on invalid input length
  • Fixed Reflect::get returning undefined instead of error for missing properties
  • Fixed ProTxHashLike rejecting ProTxHash objects
  • Fixed precision loss for large credit values
  • Fixed various parameter name inconsistencies (assetLockassetLockProof, recipientIdrecipientOwnerId, etc.)
  • Fixed setters calling wrong methods
  • Fixed getters taking &mut self instead of &self

Dependencies

  • Updated TypeScript version
  • Updated various npm dependencies

How Has This Been Tested?

  • All 718 wasm-dpp2 unit tests pass
  • All js-evo-sdk unit tests pass
  • All wasm-sdk unit and functional tests pass
  • TypeScript type checking enabled for all test files
  • Standardized test descriptions to use "should ..." format
  • Refactored tests to use nested describe blocks by method/feature
  • Fixed duplicate assertions, misleading test names, and $format_version field names
  • Added tests for token result types, conversion methods, and edge cases

Breaking Changes

Dash Platform Protocol entities interface was changed.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • Chores
    • Upgraded ESLint to version 9.18.0 with modernized configuration system
    • Updated default Node.js runtime version to 24
    • Updated tar dependency to 7.5.7
    • Migrated ESLint configuration to flat config format across all packages

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

This pull request upgrades Node.js from version 20 to 24, migrates ESLint from v8 to v9 with new configuration modules, removes legacy ESLint configuration files across packages, and updates dependencies including tar. The changes involve consolidating ESLint config into new eslint.config.mjs files while removing old .eslintrc files, plus updating linting rule references from import/ to import-x/.

Changes

Cohort / File(s) Summary
ESLint Configuration Modules
eslint/base.mjs, eslint/mocha-tests.mjs, eslint/typescript.mjs, eslint/wasm.mjs
New ESLint v9 configuration modules introduced. Base config aggregates plugins and rule sets; mocha-tests relaxes rules for test files; typescript handles TypeScript-specific rules; wasm relaxes rules for WASM bindings.
Root Package Configuration
package.json, .pnp.cjs
Added eslint v9.18.0, @typescript-eslint/parser, eslint-config-airbnb-extended, globals, and typescript to devDependencies. Updated tar resolution to 7.5.7. Minor changes to .pnp.cjs runtime state.
GitHub Actions Workflow Updates
.github/actions/nodejs/action.yaml, .github/workflows/prebuild-devcontainers.yml, .github/workflows/tests.yml
Node.js version bumped from 20 to 24 across CI/CD workflows.
Package ESLint Migrations (bench-suite)
packages/bench-suite/package.json, packages/bench-suite/.eslintrc, packages/bench-suite/eslint.config.mjs
Updated eslint to v9.18.0, removed babel-eslint and config plugins. Created new eslint.config.mjs; deleted old .eslintrc.
Package ESLint Migrations (dapi-grpc)
packages/dapi-grpc/package.json, packages/dapi-grpc/.eslintrc, packages/dapi-grpc/.eslintignore, packages/dapi-grpc/eslint.config.mjs, packages/dapi-grpc/lib/test/.eslintrc, packages/dapi-grpc/test/.eslintrc
Updated eslint to v9.18.0. Removed legacy .eslintrc files and .eslintignore patterns. Created new eslint.config.mjs.
Package ESLint Migrations (dapi)
packages/dapi/package.json, packages/dapi/.eslintrc, packages/dapi/.eslintignore, packages/dapi/eslint.config.mjs, packages/dapi/lib/test/.eslintrc, packages/dapi/test/.eslintrc
Updated eslint to v9.18.0, removed @babel/eslint-parser and config plugins. Removed coverage from .eslintignore. Deleted legacy .eslintrc files; created new eslint.config.mjs.
Package ESLint Migrations (dash-spv)
packages/dash-spv/package.json, packages/dash-spv/.eslintrc.json, packages/dash-spv/.eslintignore, packages/dash-spv/eslint.config.mjs, packages/dash-spv/lib/test/.eslintrc
Updated eslint to v9.18.0, removed config plugins. Removed dist/ and .nyc_output/ from .eslintignore. Deleted legacy config files; created new eslint.config.mjs.
Package ESLint Migrations (dashmate)
packages/dashmate/package.json, packages/dashmate/.eslintrc, packages/dashmate/eslint.config.mjs, packages/dashmate/src/test/.eslintrc, packages/dashmate/test/.eslintrc
Updated eslint to v9.18.0, removed @babel/eslint-parser and config plugins. Updated tar to 7.5.7. Deleted legacy .eslintrc files; created new eslint.config.mjs with import-x/extensions rule.
Package ESLint Migrations (dashpay-contract)
packages/dashpay-contract/package.json, packages/dashpay-contract/.eslintrc, packages/dashpay-contract/eslint.config.mjs, packages/dashpay-contract/lib/test/.eslintrc
Updated eslint to v9.18.0, removed config plugins. Deleted legacy .eslintrc files; created new eslint.config.mjs.
Linting Rule Updates
packages/bench-suite/lib/Runner.js, packages/dashmate/src/commands/setup.js, packages/dashmate/src/status/enums/*, packages/dashmate/test/bootstrap.js
Updated ESLint disable directives from import/ to import-x/ rule names to match new plugin.
Code Cleanup
packages/bench-suite/benchmarks/basicValidation.js, packages/bench-suite/benchmarks/index.js, packages/bench-suite/lib/metrics/drive/DriveMetricsCollector.js
Updated imports to remove unused exports. Removed eslint-disable comment. Replaced typed catch with bare catch in error handler.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A Tale of Linting Cheer

From ESLint eight to nine we hop,
With Node.js twenty-four we don't stop,
Old configs gone, new .mjs in place,
import-x rules bring style and grace,
The rabbit's config dance runs just right! 🎩✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The PR title "refactor(sdk)!: comprehensive Evo SDK refactoring" clearly describes the main change—a comprehensive refactoring of the Evo SDK with breaking changes (indicated by the '!' marker).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added this to the v3.0.0 milestone Jan 16, 2026
@shumkov shumkov self-assigned this Jan 17, 2026
@thephez thephez changed the title refactor(sdk): standartize dpp2 api refactor(sdk): standardize dpp2 api Jan 20, 2026
@shumkov shumkov changed the title refactor(sdk): standardize dpp2 api refactor(sdk): standardize dpp2 api [WIP] Jan 21, 2026
@shumkov shumkov marked this pull request as ready for review January 21, 2026 07:26
@shumkov
Copy link
Collaborator Author

shumkov commented Jan 21, 2026

@CodeRabbit review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs (1)

153-156: Fix copy-paste bug: calling wrong setter method.

The set_minimum_token_cost function incorrectly calls set_maximum_token_cost on the inner type, which would cause the minimum cost value to be set as the maximum cost instead.

🐛 Proposed fix
 #[wasm_bindgen(setter = "minimumTokenCost")]
 pub fn set_minimum_token_cost(&mut self, minimum_cost: Option<TokenAmount>) {
-    self.0.set_maximum_token_cost(minimum_cost);
+    self.0.set_minimum_token_cost(minimum_cost);
 }
packages/wasm-dpp2/src/enums/batch/gas_fees_paid_by.rs (1)

52-55: Incorrect error message references "batch type" instead of "gas fees".

The error messages say "unknown batch type value" but this enum is GasFeesPaidBy. This appears to be a copy-paste error.

Fix error message
-                _ => Err(WasmDppError::invalid_argument(format!(
-                    "unknown batch type value: {}",
-                    enum_val
-                ))),
+                _ => Err(WasmDppError::invalid_argument(format!(
+                    "unknown gas fees paid by value: {}",
+                    enum_val
+                ))),

Apply the same fix at line 64-67.

Also applies to: 64-67

packages/wasm-dpp2/src/tokens/configuration/distribution_recipient.rs (1)

45-55: recipientType should stay a stable variant name.
Returning "Identity(<id>)" couples type and value, which breaks checks like recipientType === "Identity". The identifier is already exposed via value, so this should return just "Identity".

Proposed fix
-            TokenDistributionRecipient::Identity(identity) => {
-                format!("Identity({})", IdentifierWasm::from(identity).to_base58())
-            }
+            TokenDistributionRecipient::Identity(_) => String::from("Identity"),
packages/wasm-dpp2/src/state_transitions/base/state_transition.rs (1)

101-112: Change key_type parameter to use Option<KeyTypeWasm> to properly reflect optional behavior.

The method currently accepts key_type: JsValue and checks is_undefined() for the default, but this pattern doesn't ensure the generated TypeScript signature treats keyType as optional. Use Option<KeyTypeWasm> instead, matching the pattern already established in verify_public_key (line 131-132) for optional parameters, then convert using .map_or() or similar to preserve the ECDSA_SECP256K1 default.

packages/wasm-dpp2/src/data_contract/transitions/update.rs (1)

166-190: Inconsistent parameter ordering with create.rs.

The parameter order here is (full_validation, platform_version), but in create.rs (line 182-183) it's (platform_version, full_validation). This inconsistency could confuse API consumers.

Suggested fix to align parameter ordering
     #[wasm_bindgen(js_name = "getDataContract")]
     pub fn get_data_contract(
         &self,
-        full_validation: Option<bool>,
         platform_version: PlatformVersionLikeJs,
+        full_validation: Option<bool>,
     ) -> WasmDppResult<DataContractWasm> {
packages/wasm-dpp2/src/asset_lock_proof/outpoint.rs (1)

106-128: Guard fromBytes against oversized input.

Line 111 slices with len from user input; len > 36 will panic in WASM. Prefer explicit validation and return a structured error.

🐛 Suggested fix (validate length and return error)
 #[wasm_bindgen(js_name = "fromBytes")]
-pub fn from_bytes(buffer: Vec<u8>) -> OutPointWasm {
+pub fn from_bytes(buffer: Vec<u8>) -> WasmDppResult<OutPointWasm> {
+    if buffer.len() != 36 {
+        return Err(WasmDppError::invalid_argument(format!(
+            "OutPoint bytes must be 36 bytes, got {}",
+            buffer.len()
+        )));
+    }
     let mut out_buffer = [0u8; 36];
-    let bytes = buffer.as_slice();
-    let len = bytes.len();
-    out_buffer[..len].copy_from_slice(bytes);
-
-    OutPointWasm(OutPoint::from(out_buffer))
+    out_buffer.copy_from_slice(&buffer);
+
+    Ok(OutPointWasm(OutPoint::from(out_buffer)))
 }
@@
-    Ok(OutPointWasm::from_bytes(bytes))
+    OutPointWasm::from_bytes(bytes)
@@
-    Ok(OutPointWasm::from_bytes(bytes))
+    OutPointWasm::from_bytes(bytes)
packages/wasm-dpp2/src/identity/partial_identity.rs (1)

279-323: Accept numeric string keys in Object::keys iteration

Object::keys returns strings regardless of key type, so key.as_f64() will always fail. Parse string keys (or numeric values if somehow present) into u32.

🐛 Suggested fix (accept numeric string keys)
-            let key_val = key.as_f64().ok_or_else(|| {
-                WasmDppError::invalid_argument("Key identifier must be numeric")
-            })?;
-
-            if key_val > u32::MAX as f64 {
-                return Err(WasmDppError::invalid_argument(format!(
-                    "Key id '{:?}' exceeds the maximum limit for u32.",
-                    key.as_string()
-                )));
-            }
-
-            let key_id = KeyID::from(key_val as u32);
+            let key_id_u32: u32 = if let Some(n) = key.as_f64() {
+                if n.fract() != 0.0 || n < 0.0 || n > u32::MAX as f64 {
+                    return Err(WasmDppError::invalid_argument(
+                        "Key identifier must be a valid u32",
+                    ));
+                }
+                n as u32
+            } else if let Some(s) = key.as_string() {
+                s.parse().map_err(|_| {
+                    WasmDppError::invalid_argument("Key identifier must be numeric")
+                })?
+            } else {
+                return Err(WasmDppError::invalid_argument("Key identifier must be numeric"));
+            };
+
+            let key_id = KeyID::from(key_id_u32);
@@
-                        key_val as u32, message
+                        key_id_u32, message
🤖 Fix all issues with AI agents
In `@packages/wasm-dpp2/src/core/pro_tx_hash.rs`:
- Around line 34-67: The TryFrom implementation for ProTxHashLikeJs currently
only routes to ProTxHashWasm::try_from(JsValue) and therefore rejects an input
that is already a ProTxHash JS object; add a fast-path that detects a JS object
convertible via the IntoWasm trait (as done in IdentifierWasm) and convert it
directly to ProTxHashWasm before falling back to the hex/Uint8Array parsing;
update TryFrom<ProTxHashLikeJs> for ProTxHashWasm to call IntoWasm::into_wasm
when the JsValue is an object representing a ProTxHash, and mirror the same
pattern in TryFrom<ProTxHashLikeNullableJs> path so ProTxHash JS objects and
nullable/empty-string cases are handled correctly.

In `@packages/wasm-dpp2/src/data_contract/document/model.rs`:
- Around line 173-210: The constructor currently converts revision_js via
as_f64() and casts to u64 which can silently accept fractional/invalid values;
replace that logic to call crate::utils::try_to_u64(revision_js) and wrap the
result with Revision::from(...) (mirror the pattern used in
partial_identity.rs). Ensure you reference the local variable revision_js,
remove the direct as_f64() cast, and propagate any error from try_to_u64 so
invalid/NaN/negative/fractional values are rejected.

In `@packages/wasm-dpp2/src/enums/keys/purpose.rs`:
- Around line 6-11: The extern type PurposeLikeJs references a missing
TypeScript type alias "PurposeLike"; add a TS_TYPES block that defines
PurposeLike (e.g., a union of allowed values or string | number as used in other
key enum files) so the generated bindings include the TypeScript declaration;
update the same file's wasm-bindgen TS_TYPES section to declare export type
PurposeLike = /* match other enums' pattern, e.g. */ "AUTH" | "ENCRYPT" |
string; ensuring the identifier PurposeLike matches the typescript_type used on
the extern type PurposeLikeJs.

In `@packages/wasm-dpp2/src/enums/keys/security_level.rs`:
- Around line 6-11: The extern type SecurityLevelLikeJs declares typescript_type
= "SecurityLevelLike" but no TypeScript definition is emitted; add a matching
typescript_custom_section for "SecurityLevelLike" (similar to
gas_fees_paid_by.rs and key_type.rs) so consumers get the TypeScript
alias/interface. Locate the extern type SecurityLevelLikeJs and add a
#[wasm_bindgen(typescript_custom_section = r#"/* TS_TYPES */ ..."#)] attribute
that defines the SecurityLevelLike type (matching expected shape/union used
elsewhere) to ensure the generated d.ts includes the declaration.

In `@packages/wasm-dpp2/src/epoch/extended_epoch_info.rs`:
- Around line 90-94: The use of Reflect::get in the ExtendedEpochInfo
constructor (e.g., the options_obj -> Reflect::get(...)->as_f64() chain that
produces index as u16) is incorrect to treat Reflect::get errors as "missing
key" because Reflect::get returns Ok(JsValue::UNDEFINED) for absent properties;
update each property extraction (e.g., the index flow) to explicitly check for
JsValue::UNDEFINED or call .is_undefined() on the result and return
WasmDppError::invalid_argument with a clear "index is required" message when
undefined, then convert the defined value to number (using as_f64() and map to
invalid_argument "index must be a number" if None); apply the same pattern for
all other Reflect::get uses in the constructor and keep error creation via
WasmDppError::invalid_argument for consistent messages.

In `@packages/wasm-dpp2/src/state_transitions/batch/token_base_transition.rs`:
- Around line 61-66: The current extraction of tokenContractPosition casts a f64
to u16 with `as u16`, which can silently truncate; replicate the safe pattern
used for `identityContractNonce` by retrieving the value from `options_obj`, get
it as f64, convert it to an integer using the existing `try_to_u64` helper (or
the same conversion function used for `identityContractNonce`), validate range,
then explicitly cast to u16 only after successful conversion to prevent
truncation or out-of-range values when setting `token_contract_position`.
- Around line 76-87: The code currently uses
Reflect::get(...).unwrap_or(JsValue::UNDEFINED), which hides property access
errors; change it to propagate Reflect::get errors instead of swallowing them:
call Reflect::get(&options_obj, &"usingGroupInfo".into()) and propagate any Err
via the function's Result (using ? or mapping the JsValue error into the
function error), then use the returned JsValue (check is_undefined to
distinguish absence) before converting with to_wasm on
GroupStateTransitionInfoWasm and converting into GroupStateTransitionInfo;
ensure you reference the using_group_info_js variable, Reflect::get, to_wasm,
GroupStateTransitionInfoWasm, and GroupStateTransitionInfo when making the
change.

In `@packages/wasm-dpp2/src/version.rs`:
- Around line 16-41: The numeric path for PlatformVersionLike is accepting f64s
but PlatformVersionWasm::try_from currently casts f64 to u8 directly; update
PlatformVersionWasm::try_from to validate numeric inputs like NetworkWasm does:
if the incoming JsValue is a number, ensure it is an integer (no fractional
part) and within the valid u8 range (0..=255) before casting, and return a
WasmDppError for non-integer or out-of-range values; locate the conversion logic
in PlatformVersionWasm::try_from (and the TryFrom<PlatformVersionLikeJs> impl
that delegates to it) and mirror the integer validation pattern used by
NetworkWasm.
🧹 Nitpick comments (29)
packages/wasm-dpp2/JSVALUE_TYPE_AUDIT.md (1)

8-8: Clarify category numbering for readability.
Starting at “Category 2” without a preceding “Category 1” is a bit confusing in a standalone doc; consider renaming to a neutral header (e.g., “Return Types Using JsValue”) or adding Category 1 for completeness.

packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_emergency_action.rs (2)

48-51: Unnecessary clone of entire struct.

self.clone() clones the whole wrapper unnecessarily. The method only needs the inner value's public_note_owned().

♻️ Suggested fix
     #[wasm_bindgen(getter = "publicNote")]
     pub fn get_public_note(&self) -> Option<String> {
-        self.clone().0.public_note_owned()
+        self.0.public_note().cloned()
     }

If public_note_owned() requires ownership, consider using self.0.public_note().cloned() or checking if there's a borrowing accessor available.


53-56: Asymmetric getter/setter types for emergencyAction.

The getter returns String while the setter accepts TokenEmergencyActionWasm. This creates an inconsistent API. Compare with base which uses TokenBaseTransitionWasm for both getter and setter.

Consider returning TokenEmergencyActionWasm from the getter for consistency:

♻️ Suggested fix
     #[wasm_bindgen(getter = "emergencyAction")]
-    pub fn get_emergency_action(&self) -> String {
-        TokenEmergencyActionWasm::from(self.0.emergency_action()).into()
+    pub fn get_emergency_action(&self) -> TokenEmergencyActionWasm {
+        self.0.emergency_action().into()
     }
packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_claim.rs (1)

75-90: Consider extracting duplicated conversion logic.

Lines 83-86 duplicate the same conversion pattern from the constructor (lines 36-39). If this pattern appears elsewhere in the codebase, a small helper function could reduce repetition:

fn parse_distribution_type(value: &JsValue) -> WasmDppResult<TokenDistributionTypeWasm> {
    match value.is_undefined() {
        true => Ok(TokenDistributionTypeWasm::default()),
        false => TokenDistributionTypeWasm::try_from(value.clone()),
    }
}

This is a minor nit given it's only two occurrences in this file.

packages/wasm-dpp2/src/state_transitions/batch/prefunded_voting_balance.rs (1)

29-35: Unconventional Rust function name constructor.

While the #[wasm_bindgen(constructor)] attribute correctly enables JS new ClassName() syntax, the Rust function name constructor is non-idiomatic. The standard Rust convention is to name this function new, which would still work correctly with the wasm_bindgen attribute.

That said, if this is a deliberate PR-wide convention for consistency with the JS/TS API surface, it's acceptable.

packages/wasm-sdk/src/wallet/key_generation.rs (1)

118-133: Pre-allocate the vector to avoid small reallocations.
This is minor, but cheap and clean since count is bounded.

♻️ Suggested tweak
-        let mut pairs = Vec::new();
+        let mut pairs = Vec::with_capacity(count as usize);
packages/wasm-dpp2/src/tokens/configuration/pre_programmed_distribution.rs (1)

131-132: Consider validating that inner value is a Map.

Map::from(value) will attempt to create a Map from any JsValue. If the caller passes a malformed structure where the inner value is not a Map (e.g., an object or array), this won't fail immediately but will produce incorrect/empty results, making debugging difficult.

♻️ Proposed validation
-        let inner_map = Map::from(value);
+        let inner_map: Map = value.dyn_into().map_err(|_| {
+            WasmDppError::invalid_argument(format!(
+                "Expected Map for distribution amounts at timestamp '{}', got: {:?}",
+                timestamp_str,
+                value.js_typeof()
+            ))
+        })?;

This requires adding dyn_into from JsCast (already imported).

packages/wasm-dpp2/src/identity/transitions/masternode_vote_transition.rs (1)

116-132: Consider adding null checks for optional fields.

The code checks for undefined but not null for optional fields. While the TypeScript types don't allow null, JavaScript callers could still pass it:

  • For signaturePublicKeyId: passing null would trigger "must be a number" error (misleading but safe)
  • For signature: Uint8Array::from(null) may produce unexpected results

This follows the existing codebase pattern, so it's a minor edge case, but adding explicit null checks would improve robustness.

♻️ Suggested improvement
-        let signature_public_key_id: KeyID = if signature_public_key_js.is_undefined() {
+        let signature_public_key_id: KeyID = if signature_public_key_js.is_undefined() || signature_public_key_js.is_null() {
             0
         } else {
             ...
         };

-        let signature: Vec<u8> = if signature_js.is_undefined() {
+        let signature: Vec<u8> = if signature_js.is_undefined() || signature_js.is_null() {
             Vec::new()
         } else {
             Uint8Array::from(signature_js).to_vec()
         };
packages/wasm-dpp2/src/tokens/configuration/reward_distribution_type.rs (1)

60-82: Redundant clone on function in each match arm.

The function is already owned after destructuring from self.0.clone(), so the additional .clone() on lines 66, 72, and 78 is unnecessary.

♻️ Suggested refactor to remove redundant clones
     #[wasm_bindgen(getter = "distribution")]
     pub fn distribution(&self) -> JsValue {
         match self.0.clone() {
             RewardDistributionType::BlockBasedDistribution { interval, function } => {
                 JsValue::from(BlockBasedDistributionWasm {
                     interval,
-                    function: function.clone().into(),
+                    function: function.into(),
                 })
             }
             RewardDistributionType::TimeBasedDistribution { interval, function } => {
                 JsValue::from(TimeBasedDistributionWasm {
                     interval,
-                    function: function.clone().into(),
+                    function: function.into(),
                 })
             }
             RewardDistributionType::EpochBasedDistribution { interval, function } => {
                 JsValue::from(EpochBasedDistributionWasm {
                     interval,
-                    function: function.clone().into(),
+                    function: function.into(),
                 })
             }
         }
     }
packages/wasm-dpp2/src/epoch/finalized_epoch_info.rs (1)

182-243: Consider adding field context to try_to_u64 error messages.

When try_to_u64 fails (e.g., for firstBlockTime), the error message from try_to_u64 won't include the field name, making it harder for SDK users to identify which field caused the issue.

Example improvement for one field:
 let first_block_time = try_to_u64(
     Reflect::get(&options_obj, &"firstBlockTime".into())
         .map_err(|_| WasmDppError::invalid_argument("firstBlockTime is required"))?,
-)? ;
+).map_err(|e| WasmDppError::invalid_argument(format!("firstBlockTime: {}", e)))?;
packages/wasm-dpp2/src/state_transitions/batch/token_transition.rs (2)

148-185: Consider avoiding unnecessary clone in getter.

The transition getter calls self.clone().0 which clones the entire TokenTransitionWasm struct. If the From implementations for the specific transition Wasm types can work with references or if the inner TokenTransition can be cloned directly, this could be optimized.

♻️ Potential optimization
     #[wasm_bindgen(getter = "transition")]
     pub fn transition(&self) -> JsValue {
-        match self.clone().0 {
+        match &self.0 {
             TokenTransition::Burn(token_transition) => {
-                TokenBurnTransitionWasm::from(token_transition).into()
+                TokenBurnTransitionWasm::from(token_transition.clone()).into()
             }
             // ... similar for other variants
         }
     }

This avoids cloning the wrapper struct and only clones the specific inner transition that's actually needed.


187-202: Consider deriving transition type numbers to prevent protocol contract misalignment.

The numeric values 0-10 are hardcoded in the match statement without any compile-time enforcement that they stay synchronized with the TokenTransition enum variants. Since this is exposed as a WASM binding (JavaScript API), these numbers form part of the protocol contract. If enum variants are reordered or added, the numbers here won't automatically update, risking silent breakage.

Consider using explicit enum discriminants (#[repr(u8)]) or a constant mapping to ensure type numbers remain synchronized with enum changes.

packages/wasm-dpp2/src/tokens/configuration/configuration_convention.rs (1)

82-121: Validate localizations is a plain object before coercion.

Object::from will coerce primitives/arrays into objects, which can yield confusing errors. Consider rejecting non-object inputs up front to keep error messages clear and prevent accidental acceptance of invalid shapes.

♻️ Suggested guard (plus import)
-use js_sys::{Object, Reflect};
+use js_sys::{Array, Object, Reflect};
 fn value_to_localizations(
     localizations_value: &JsValue,
 ) -> WasmDppResult<BTreeMap<String, TokenConfigurationLocalization>> {
+    if localizations_value.is_null() || localizations_value.is_undefined() {
+        return Err(WasmDppError::invalid_argument(
+            "localizations must be an object",
+        ));
+    }
+    if !localizations_value.is_object() || Array::is_array(localizations_value) {
+        return Err(WasmDppError::invalid_argument(
+            "localizations must be a Record<string, TokenConfigurationLocalization>",
+        ));
+    }
     let js_object = Object::from(localizations_value.clone());
     let mut localizations = BTreeMap::new();
packages/wasm-dpp2/src/state_transitions/batch/token_pricing_schedule.rs (1)

81-86: Consider using bigint for SetPrices values for consistency.

SinglePrice returns values as bigint (line 76), but SetPrices serializes values using .into() which produces a JavaScript number. For large credit values that exceed JavaScript's safe integer range, this could cause precision loss.

♻️ Suggested fix for consistency
                 for (key, value) in prices.iter() {
                     Reflect::set(
                         &price_object,
                         &JsValue::from(key.to_string()),
-                        &(*value).into(),
+                        &JsValue::bigint_from_str(&value.to_string()),
                     )
packages/wasm-dpp2/src/epoch/extended_epoch_info.rs (1)

90-94: Silent truncation on numeric casts may lose precision.

The as u16, as u32 casts will silently truncate values exceeding their ranges:

  • Line 94: index cast to u16 (max 65,535)
  • Line 111: first_core_block_height cast to u32 (max ~4.2 billion)
  • Line 122: protocol_version cast to u32

If a JS caller passes a value like 100000 for index, it will be silently truncated.

Consider adding range validation, especially for index:

let index_f64 = index_js.as_f64()
    .ok_or_else(|| WasmDppError::invalid_argument("index must be a number"))?;
if index_f64 < 0.0 || index_f64 > u16::MAX as f64 {
    return Err(WasmDppError::invalid_argument(
        format!("index must be between 0 and {}", u16::MAX)
    ));
}
let index = index_f64 as u16;

Also applies to: 106-111, 118-122

packages/wasm-dpp2/src/state_transitions/batch/batched_transition.rs (1)

100-103: Consider simplifying the identifier conversion path.

The current conversion goes through an intermediate JsValue:

let id_value: JsValue = contract_id.into();
let contract_id: Identifier = IdentifierWasm::try_from(&id_value)?.into();

Based on the TryFrom<IdentifierLikeJs> for Identifier implementation in identifier.rs (lines 46-51), this could be simplified to:

let contract_id: Identifier = contract_id.try_into()?;
♻️ Suggested simplification
     #[wasm_bindgen(setter = "dataContractId")]
     pub fn set_data_contract_id(&mut self, contract_id: IdentifierLikeJs) -> WasmDppResult<()> {
-        let id_value: JsValue = contract_id.into();
-        let contract_id: Identifier = IdentifierWasm::try_from(&id_value)?.into();
+        let contract_id: Identifier = contract_id.try_into()?;
packages/wasm-dpp2/src/voting/vote_poll.rs (1)

82-112: Consider validating that required fields are present, not just that Reflect::get succeeds.

Reflect::get typically doesn't throw an error when a property doesn't exist—it returns Ok(JsValue::UNDEFINED). The current error messages suggest the fields are "missing" when Reflect fails, but Reflect failure usually indicates a different issue (e.g., the target is not an object).

For robustness, consider explicitly checking if the retrieved values are undefined:

♻️ Suggested validation improvement
         // Extract contractId (required)
         let js_contract_id = Reflect::get(&object, &JsValue::from_str("contractId"))
             .map_err(|e| WasmDppError::invalid_argument(format!("Missing contractId: {:?}", e)))?;
+        if js_contract_id.is_undefined() {
+            return Err(WasmDppError::invalid_argument("contractId is required"));
+        }
         let contract_id = IdentifierWasm::try_from(&js_contract_id)?.into();

         // Extract indexValues (required)
         let js_index_values = Reflect::get(&object, &JsValue::from_str("indexValues"))
             .map_err(|e| WasmDppError::invalid_argument(format!("Missing indexValues: {:?}", e)))?;
+        if js_index_values.is_undefined() {
+            return Err(WasmDppError::invalid_argument("indexValues is required"));
+        }
packages/wasm-dpp2/src/identifier.rs (1)

339-355: Consider simplifying the error mapping in identifiers_from_js_array.

The error is already a WasmDppError from IdentifierWasm::try_from, so wrapping it in another invalid_argument adds redundant context. The inner error already contains "Invalid identifier" messaging.

♻️ Suggested simplification
 pub fn identifiers_from_js_array(array: IdentifierLikeArrayJs) -> WasmDppResult<Vec<Identifier>> {
     let js_value: JsValue = array.into();
     let js_array = js_sys::Array::from(&js_value);
     js_array
         .iter()
-        .map(|v| {
-            IdentifierWasm::try_from(v)
-                .map(Identifier::from)
-                .map_err(|err| {
-                    WasmDppError::invalid_argument(format!("Invalid identifier: {}", err))
-                })
-        })
+        .map(|v| IdentifierWasm::try_from(v).map(Identifier::from))
         .collect()
 }
packages/wasm-dpp2/src/tokens/configuration_change_item/items/new_tokens_destination_identity.rs (1)

2-20: Align optional identity semantics with the signature.
The function accepts IdentifierLikeJs (required) but still treats undefined as None, while null will error. If the value is intentionally optional, consider using IdentifierLikeOrUndefinedJs (or Option<IdentifierLikeJs>) so the signature matches behavior and null/undefined are handled consistently.

Proposed refactor
-use crate::identifier::{IdentifierLikeJs, IdentifierWasm};
+use crate::identifier::{IdentifierLikeOrUndefinedJs, IdentifierWasm};
@@
     #[wasm_bindgen(js_name = "NewTokensDestinationIdentityItem")]
     pub fn new_tokens_destination_identity_item(
-        identity_id: IdentifierLikeJs,
+        identity_id: IdentifierLikeOrUndefinedJs,
     ) -> WasmDppResult<TokenConfigurationChangeItemWasm> {
-        let id_value: JsValue = identity_id.into();
-        let identity_id: Option<Identifier> = match id_value.is_undefined() {
-            true => None,
-            false => Some(IdentifierWasm::try_from(&id_value)?.into()),
-        };
+        let identity_id: Option<Identifier> = identity_id.try_into()?;
packages/wasm-sdk/src/queries/group.rs (2)

798-835: Consider hoisting the inline import to the top-level imports.

The function correctly uses identifiers_from_js_array for batch conversion. However, the inline use statement on line 803 could be moved to the top-level imports (line 20) for consistency with Rust conventions and to avoid redundant imports if this pattern is used elsewhere.

♻️ Suggested refactor

Move the import to the top of the file:

-use wasm_dpp2::identifier::{IdentifierLikeArrayJs, IdentifierLikeJs, IdentifierWasm};
+use wasm_dpp2::identifier::{identifiers_from_js_array, IdentifierLikeArrayJs, IdentifierLikeJs, IdentifierWasm};

Then remove the inline import on line 803.


1092-1139: Duplicate inline import - reinforces need to hoist.

This function duplicates the inline use wasm_dpp2::identifier::identifiers_from_js_array statement. Moving it to the top-level imports would eliminate this duplication.

packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_transfer.rs (1)

178-216: Consider handling null in addition to undefined for note setters.

The setters for sharedEncryptedNote and privateEncryptedNote only check is_undefined(). In JavaScript, callers might pass null to explicitly clear a value. Currently, passing null would attempt conversion and likely fail.

♻️ Suggested fix
     pub fn set_shared_encrypted_note(
         &mut self,
         shared_encrypted_note: &JsValue,
     ) -> WasmDppResult<()> {
         let shared_encrypted_note: Option<SharedEncryptedNote> =
-            match shared_encrypted_note.is_undefined() {
+            match shared_encrypted_note.is_undefined() || shared_encrypted_note.is_null() {
                 true => None,
                 false => Some(
                     shared_encrypted_note
                         .to_wasm::<SharedEncryptedNoteWasm>("SharedEncryptedNote")?
                         .clone()
                         .into(),
                 ),
             };

Apply the same pattern to set_private_encrypted_note.

packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs (1)

218-231: Consider handling null for output script setter.

Similar to the token transfer note setters, set_output_script only checks is_undefined(). JavaScript callers might pass null to clear the output script.

♻️ Suggested fix
     pub fn set_output_script(
         &mut self,
         #[wasm_bindgen(unchecked_param_type = "CoreScript | undefined")] script: &JsValue,
     ) -> WasmDppResult<()> {
-        if script.is_undefined() {
+        if script.is_undefined() || script.is_null() {
             self.0.set_output_script(None);
         } else {
packages/wasm-dpp2/src/identity/transitions/update_transition.rs (1)

227-233: Consider simplifying identifier conversion.

The setter converts IdentifierLikeJsJsValueIdentifierWasm. Based on the TryFrom<IdentifierLikeJs> impl in identifier.rs (lines 39-42), you can convert directly.

♻️ Simplified conversion
     pub fn set_identity_identifier(&mut self, identity_id: IdentifierLikeJs) -> WasmDppResult<()> {
-        let id_value: JsValue = identity_id.into();
-        let identity_id = IdentifierWasm::try_from(&id_value)?.into();
+        let identity_id: dpp::platform_value::Identifier = identity_id.try_into()?;
         self.0.set_identity_id(identity_id);
         Ok(())
     }
packages/wasm-dpp2/src/public_key.rs (1)

88-117: Consider documenting the auto-detection behavior in set_inner.

The setter infers compression from byte length (33 = compressed, 65 = uncompressed), which differs from the constructor's explicit compressed parameter. This is pragmatic but could surprise callers. Consider adding a JSDoc comment to clarify this behavior.

packages/wasm-dpp2/src/group/action_event.rs (1)

67-72: Consider returning Option<TokenEventWasm> for future-proofing.

Currently token_event() unconditionally returns TokenEventWasm since there's only one variant. If additional variants are added to GroupActionEvent in the future, this method would need updating. Returning Option<TokenEventWasm> would be more defensive.

Optional: Return Option for variant-specific accessor
     #[wasm_bindgen(js_name = "tokenEvent")]
-    pub fn token_event(&self) -> TokenEventWasm {
+    pub fn token_event(&self) -> Option<TokenEventWasm> {
         match &self.0 {
-            GroupActionEvent::TokenEvent(event) => TokenEventWasm::from(event.clone()),
+            GroupActionEvent::TokenEvent(event) => Some(TokenEventWasm::from(event.clone())),
         }
     }
packages/wasm-dpp2/src/identity/model.rs (1)

122-129: Consider keeping a backward-compatible getPublicKeys alias.

If older JS consumers rely on getPublicKeys, an alias eases migration; otherwise please document the break.

♻️ Optional compatibility alias
 #[wasm_bindgen(getter = "publicKeys")]
 pub fn public_keys(&self) -> Vec<IdentityPublicKeyWasm> {
     self.0
         .public_keys()
         .values()
         .map(|key| IdentityPublicKeyWasm::from(key.clone()))
         .collect()
 }
+
+#[wasm_bindgen(js_name = "getPublicKeys")]
+pub fn get_public_keys(&self) -> Vec<IdentityPublicKeyWasm> {
+    self.public_keys()
+}
packages/wasm-dpp2/src/identity/public_key.rs (2)

124-128: Missing validation for non-string input types could cause silent failures.

When the input is not a string, js_sys::Uint8Array::new(&js_value) is called without verifying the input is actually a Uint8Array. This can produce unexpected results:

  • A number n creates a zero-filled array of length n
  • An object or null may create an empty array

While callers like get_identity_by_public_key_hash validate the 20-byte length afterward, this function could silently accept invalid inputs and return misleading data.

♻️ Suggested validation
     } else {
-        // Try to convert as Uint8Array
-        let array = js_sys::Uint8Array::new(&js_value);
-        Ok(array.to_vec())
+        // Try to convert as Uint8Array
+        if js_value.is_instance_of::<js_sys::Uint8Array>() {
+            let array = js_sys::Uint8Array::unchecked_from_js(js_value);
+            Ok(array.to_vec())
+        } else {
+            Err(WasmDppError::invalid_argument(
+                "Public key hash must be a hex string or Uint8Array",
+            ))
+        }
     }

154-169: Consider clarifying error handling for missing required fields.

Reflect::get returns Ok(JsValue::UNDEFINED) for missing properties, not Err. The current error mapping on lines 156, 161-163, and 168 only catches cases where the target isn't an object. If purpose, securityLevel, or keyType are missing, the error will come from the subsequent TryFrom conversion (e.g., PurposeWasm::try_from(undefined)), not from these error handlers.

This isn't a bug since invalid inputs will still be rejected, but the error messages may differ from expectations.

♻️ Optional: Explicit undefined check
         let js_purpose = Reflect::get(&object, &JsValue::from_str("purpose"))
-            .map_err(|e| WasmDppError::invalid_argument(format!("Missing purpose: {:?}", e)))?;
+            .unwrap_or(JsValue::UNDEFINED);
+        if js_purpose.is_undefined() {
+            return Err(WasmDppError::invalid_argument("Missing required field: purpose"));
+        }
         let purpose = PurposeWasm::try_from(js_purpose)?;

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
packages/wasm-dpp2/src/enums/batch/gas_fees_paid_by.rs (1)

59-68: Validate numeric JsValue before casting to enum.

as_f64() followed by as u8 will silently coerce values like 1.7 to 1 and -1 to 255, producing incorrect variants or mismapped errors. Please validate that the number is finite and an integer within range before casting.

🛠️ Proposed fix
-        if let Some(enum_val) = value.as_f64() {
-            return match enum_val as u8 {
-                0 => Ok(GasFeesPaidByWasm::DocumentOwner),
-                1 => Ok(GasFeesPaidByWasm::ContractOwner),
-                2 => Ok(GasFeesPaidByWasm::PreferContractOwner),
-                _ => Err(WasmDppError::invalid_argument(format!(
-                    "unknown batch type value: {}",
-                    enum_val
-                ))),
-            };
-        }
+        if let Some(enum_val) = value.as_f64() {
+            if !enum_val.is_finite() || enum_val.fract() != 0.0 {
+                return Err(WasmDppError::invalid_argument(format!(
+                    "unknown batch type value: {}",
+                    enum_val
+                )));
+            }
+            return match enum_val as i64 {
+                0 => Ok(GasFeesPaidByWasm::DocumentOwner),
+                1 => Ok(GasFeesPaidByWasm::ContractOwner),
+                2 => Ok(GasFeesPaidByWasm::PreferContractOwner),
+                _ => Err(WasmDppError::invalid_argument(format!(
+                    "unknown batch type value: {}",
+                    enum_val
+                ))),
+            };
+        }
packages/wasm-dpp2/src/state_transitions/batch/token_pricing_schedule.rs (1)

78-96: Inconsistent JS number representation for Credits values may cause precision loss.

In the SinglePrice branch (line 76), Credits is correctly serialized as a bigint using bigint_from_str. However, in the SetPrices branch (line 85), (*value).into() converts Credits (u64) to a JavaScript number via JsValue::from(u64), which uses f64 internally. This can lose precision for values exceeding Number.MAX_SAFE_INTEGER (2^53 - 1).

For consistency and correctness, both branches should serialize Credits as bigint.

Proposed fix
                 for (key, value) in prices.iter() {
                     Reflect::set(
                         &price_object,
                         &JsValue::from(key.to_string()),
-                        &(*value).into(),
+                        &JsValue::bigint_from_str(&value.to_string()),
                     )
                     .map_err(|err| {
packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs (1)

153-156: Bug: set_minimum_token_cost calls set_maximum_token_cost instead.

This is a copy-paste error. The setter for minimumTokenCost incorrectly mutates the maximum cost field.

🐛 Proposed fix
     #[wasm_bindgen(setter = "minimumTokenCost")]
     pub fn set_minimum_token_cost(&mut self, minimum_cost: Option<TokenAmount>) {
-        self.0.set_maximum_token_cost(minimum_cost);
+        self.0.set_minimum_token_cost(minimum_cost);
     }
packages/wasm-dpp2/src/core/core_script.rs (1)

36-60: Add input validation for hash lengths to match PlatformAddress pattern.

Both from_p2pkh and from_p2sh silently truncate inputs longer than 20 bytes via .min(). This is inconsistent with similar functions in packages/wasm-dpp2/src/platform_address/address.rs (from_p2pkh_hash and from_p2sh_hash), which validate the input length and return a WasmDppError if the hash is not exactly 20 bytes.

If a caller passes an incorrect length (e.g., 32-byte SHA256 instead of 20-byte HASH160), the current implementation silently truncates without error, potentially hiding bugs. Add explicit length validation and return an error for incorrect sizes:

pub fn from_p2pkh(key_hash: Vec<u8>) -> WasmDppResult<Self> {
    if key_hash.len() != 20 {
        return Err(WasmDppError::invalid_argument(
            "key_hash must be exactly 20 bytes".to_string()
        ));
    }
    // ... rest of implementation
}
packages/wasm-dpp2/src/core/private_key.rs (1)

101-104: Align to_hex() output case with the parallel method in public_key.rs, which returns lowercase hex.

The to_hex() method in private_key.rs uses Case::Upper (uppercase), while the parallel to_hex() method in public_key.rs (line 355) uses hex::encode(), which defaults to lowercase. This inconsistency within the same module violates the principle of least surprise. Additionally, the codebase predominantly uses Case::Lower for hex encoding (3 out of 3 other instances). Change to Case::Lower for consistency with both the parallel method and codebase convention.

packages/wasm-dpp2/src/tokens/configuration/distribution_function.rs (1)

68-103: Validate steps_with_amount is an object before iterating.

With an unchecked param type, non-object inputs can silently yield an empty map and an unexpected distribution function. Consider an explicit guard.

🛠️ Suggested validation guard
 pub fn stepwise(
     #[wasm_bindgen(unchecked_param_type = "Record<string, bigint | number>")]
     steps_with_amount: JsValue,
 ) -> WasmDppResult<DistributionFunctionWasm> {
-    let obj = Object::from(steps_with_amount);
+    if steps_with_amount.is_null()
+        || steps_with_amount.is_undefined()
+        || !steps_with_amount.is_object()
+    {
+        return Err(WasmDppError::invalid_argument(
+            "steps_with_amount must be an object mapping step to amount",
+        ));
+    }
+    let obj = Object::from(steps_with_amount);
packages/wasm-dpp2/src/state_transitions/batch/document_base_transition.rs (1)

60-101: Explicitly guard missing documentId / dataContractId.

Reflect::get returns undefined without error when a property is missing. The current flow then defers to IdentifierWasm::try_from, which yields a less clear error. Add a direct null/undefined check for clearer diagnostics.

🛠️ Suggested guard
 let js_document_id = Reflect::get(&object, &JsValue::from_str("documentId"))
     .map_err(|e| WasmDppError::invalid_argument(format!("Missing documentId: {:?}", e)))?;
+if js_document_id.is_null() || js_document_id.is_undefined() {
+    return Err(WasmDppError::invalid_argument("documentId is required"));
+}
 let document_id = IdentifierWasm::try_from(&js_document_id)?.into();

 let js_data_contract_id = Reflect::get(&object, &JsValue::from_str("dataContractId"))
     .map_err(|e| {
         WasmDppError::invalid_argument(format!("Missing dataContractId: {:?}", e))
     })?;
+if js_data_contract_id.is_null() || js_data_contract_id.is_undefined() {
+    return Err(WasmDppError::invalid_argument("dataContractId is required"));
+}
 let data_contract_id = IdentifierWasm::try_from(&js_data_contract_id)?.into();
packages/wasm-dpp2/src/identity/partial_identity.rs (1)

279-354: Add validation to reject negative or fractional key IDs before casting to u32.

The code casts floats to u32 without validation, allowing fractional values to truncate silently (e.g., 1.51) and negative values to wrap, potentially causing key ID collisions. This pattern is already properly validated elsewhere in the codebase (see packages/wasm-dpp2/src/core/network.rs).

Proposed fix
                let key_val = key.as_f64().ok_or_else(|| {
                    WasmDppError::invalid_argument("Key identifier must be numeric")
                })?;
+                if key_val.fract() != 0.0 || key_val < 0.0 {
+                    return Err(WasmDppError::invalid_argument(
+                        "Key identifier must be a non-negative integer",
+                    ));
+                }

                 if key_val > u32::MAX as f64 {
                     return Err(WasmDppError::invalid_argument(format!(
                         "Key id '{:?}' exceeds the maximum limit for u32.",
                         key.as_string()
                     )));
                 }
                    let key_val = key.as_f64().ok_or_else(|| {
                        WasmDppError::invalid_argument("Key id must be a numeric value")
                    })?;
+                    if key_val.fract() != 0.0 || key_val < 0.0 {
+                        return Err(WasmDppError::invalid_argument(
+                            "Key id must be a non-negative integer",
+                        ));
+                    }

                     if key_val > u32::MAX as f64 {
                         return Err(WasmDppError::invalid_argument(format!(
                             "Key id '{:?}' exceeds the maximum limit for u32.",
                             key.as_string()
                         )));
                     }
packages/wasm-dpp2/src/data_contract/document/model.rs (1)

500-542: fromObject always uses PlatformVersion::latest() while similar methods accept a version parameter.

Unlike fromBytes, fromHex, and fromBase64 which accept a platform_version parameter, fromObject (line 534) and fromJSON (line 587) hardcode PlatformVersion::latest(). This inconsistency could cause issues when deserializing documents created with older platform versions.

Consider adding an optional platform_version parameter to fromObject and fromJSON for consistency with the binary deserialization methods.

🤖 Fix all issues with AI agents
In `@packages/wasm-dpp2/src/asset_lock_proof/outpoint.rs`:
- Around line 106-114: The from_bytes implementation on OutPointWasm must
validate the input length instead of unconditionally slicing; change from_bytes
to return a Result (e.g., Result<OutPointWasm, JsValue>), check that
buffer.len() == 36 and return an Err with a clear message for invalid lengths,
otherwise construct the 36-byte array and create OutPoint::from; also update the
related from_hex and from_base64 helper functions to propagate this Result
instead of assuming success so callers receive proper errors.

In `@packages/wasm-dpp2/src/asset_lock_proof/proof.rs`:
- Around line 150-158: The getters instant_lock_proof and chain_lock_proof
currently call the panic-prone From<AssetLockProof> conversion (which panics for
the wrong variant); change each getter to perform a safe runtime check of the
inner AssetLockProof variant (use the same enum/method used by the From impl)
and return a non-panicking value: either return
Option<InstantAssetLockProofWasm>/Option<ChainAssetLockProofWasm> (annotated
with #[wasm_bindgen(js_name = ...)] so JS sees null when absent) or return
JsValue::NULL when the variant doesn't match; ensure you do not call the From
conversion unless the variant matches and reference the AssetLockProof
enum/variant matching logic used in the From<AssetLockProof> implementation.

In `@packages/wasm-dpp2/src/data_contract/document/model.rs`:
- Around line 177-196: The Reflect::get calls (producing properties_js,
document_type_name retrieval, data_contract_id_js, owner_id_js) can return
JsValue::UNDEFINED for missing properties so add explicit undefined checks after
each Reflect::get (e.g., if properties_js.is_undefined() { return
Err(WasmDppError::invalid_argument("Missing 'properties' field")) } and
similarly for documentTypeName, dataContractId, ownerId) before further
conversions; for documentTypeName check undefined first then call
.as_string().ok_or_else(...) as currently done, and for data_contract_id_js and
owner_id_js return the specific "Missing '...'" error if undefined before
calling IdentifierWasm::try_from.

In `@packages/wasm-dpp2/src/data_contract/model.rs`:
- Around line 180-188: Reflect::get returns Ok(JsValue::UNDEFINED) for missing
properties, so update the ownerId and schemas extraction to explicitly check the
returned JsValue for undefined (e.g., js_owner_id.is_undefined() /
js_schema.is_undefined()) and return a clear WasmDppError::invalid_argument when
missing; for ownerId keep using try_into() only after confirming js_owner_id is
not undefined, and for schemas call
serialization::platform_value_from_object(js_schema) only after confirming
js_schema is not undefined so you produce explicit "missing ownerId" / "missing
schemas" errors instead of obscure try_into/serialization errors.

In `@packages/wasm-dpp2/src/epoch/extended_epoch_info.rs`:
- Around line 106-122: The conversions for firstCoreBlockHeight and
protocolVersion currently cast f64 to u32 without bounds checks; add validation
like you did for index (or implement a reusable helper) to ensure the f64 is
finite, non-negative, an integer, and <= u32::MAX before casting. Update the
parsing of "firstCoreBlockHeight" and "protocolVersion" (and/or introduce a
helper used by both) to return a clear WasmDppError on invalid range or type
instead of silent truncation.
- Around line 90-94: The current extraction of index via
Reflect::get(...).as_f64().ok_or(...)? as u16 can silently truncate large or
non-integer JS numbers; modify the logic that reads `index` from `options_obj`
(the Reflect::get call and the `index` binding) to: convert to f64, verify it's
finite, an integer (e.g., floor equality), and within 0..=u16::MAX (65535); if
any check fails, return a WasmDppError::invalid_argument with a clear message,
otherwise cast safely to u16. This ensures `index` cannot overflow or be a
fractional/NaN/Infinity value when assigned to the `u16` variable.

In `@packages/wasm-dpp2/src/identity/public_key.rs`:
- Around line 115-129: The public_key_hash_from_js function accepts arbitrary
JsValue and can coerce non-Uint8Array values into empty arrays and doesn't
enforce HASH160's 20-byte length; update public_key_hash_from_js to first check
if js_value.as_string() yields a hex string and decode it, else verify
js_value.is_instance_of::<js_sys::Uint8Array>() before calling
js_sys::Uint8Array::new(&js_value), convert to Vec<u8>, and then validate the
resulting Vec length equals 20 (HASH160) returning a
WasmDppError::invalid_argument on failure; follow the same input-type check and
length validation pattern used in pro_tx_hash.rs to ensure callers no longer
need to duplicate this logic.

In `@packages/wasm-dpp2/src/identity/transitions/masternode_vote_transition.rs`:
- Around line 116-124: Replace the loose as_f64() parsing for
signaturePublicKeyId with the same robust validation used for nonce: call
try_to_u64 on signature_public_key_js (the variable currently named
signature_public_key_js) when it's not undefined, return a
WasmDppError::invalid_argument if conversion fails, then convert the resulting
u64 into the KeyID type (u32) with an explicit range check to ensure it fits;
update the variable signature_public_key_id to use this validated conversion so
fractional/negative/NaN/Infinity values are rejected consistently.
- Around line 99-115: After each Reflect::get call for pro_tx_hash_js,
voter_identity_id_js, vote_js and the nonce JsValue, add an explicit
undefined/null check (like the existing signaturePublicKeyId handling) and
return WasmDppError::invalid_argument("<field> is required") if the value is
undefined or null; then proceed to call IdentifierWasm::try_from for
pro_tx_hash_js and voter_identity_id_js, vote_js.to_wasm::<VoteWasm>("Vote") for
vote_js, and try_to_u64 for the nonce value so the conversions only run on
present values and produce your intended "X is required" errors.

In
`@packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_transfer.rs`:
- Around line 75-81: The code currently treats any non-string publicNote as
undefined and silently drops it; update the parsing logic around
Reflect::get(&options_obj, &"publicNote".into()) so that if
public_note_js.is_undefined() you return None, if public_note_js.is_string() you
set public_note = Some(public_note_js.as_string().unwrap()), and otherwise
return a descriptive type error (e.g., throw/return a Js TypeError) indicating
"publicNote must be a string" instead of converting to None; use the existing
variables public_note_js and public_note to locate and change the logic.

In `@packages/wasm-dpp2/src/tokens/configuration/distribution_rules.rs`:
- Around line 194-197: The getter name is inconsistent with the TypeScript
constructor option; change the wasm getter and Rust method to match the TS
property `mintingAllowChoosingDestination`: rename the #[wasm_bindgen(getter =
"...")] attribute and the Rust method `is_minting_allowing_choosing_destination`
(and any corresponding setter method/attribute) to use
`mintingAllowChoosingDestination`, and update any references to the old name so
the wasm interface and the TS constructor option use the same identifier.
- Around line 74-80: Reflect::get returns Ok(undefined) for missing properties,
so update the extraction for perpetual_distribution_rules (and the other
required fields with the same pattern) to explicitly check for undefined before
calling to_wasm: call Reflect::get(&options_obj,
&"perpetualDistributionRules".into())?, bind the JsValue, if
js_value.is_undefined() return
WasmDppError::invalid_argument("perpetualDistributionRules is required"),
otherwise call js_value.to_wasm::<ChangeControlRulesWasm>("ChangeControlRules")
and clone; apply the same pattern to the other fields referenced in this file
(the other Reflect::get usages around the noted line ranges) so missing
properties yield clear invalid_argument errors instead of relying on to_wasm.

In `@packages/wasm-dpp2/src/tokens/configuration/group.rs`:
- Around line 125-135: The get_members function currently returns a JS Map keyed
by Identifier JS objects which breaks value-based lookups; update the API so
keys are stable strings (e.g., base58 of the Identifier) or provide a helper
lookup method: modify get_members (and the GroupMembersMapJs return
type/contract) to insert keys as base58 strings via IdentifierWasm (use
IdentifierWasm -> base58 representation) instead of JS Identifier objects, or
alternatively add a new method like get_member_required_power(identifier:
&IdentifierWasm) / getMemberRequiredPower(identifier) that performs a
value-based lookup against the internal members map and returns the number, and
keep setMemberRequiredPower as-is to avoid reference-equality pitfalls.

In `@packages/wasm-dpp2/src/tokens/configuration/reward_distribution_type.rs`:
- Around line 144-148: The file contains duplicate impl blocks caused by the
macro invocations impl_wasm_type_info!(RewardDistributionTypeWasm,
RewardDistributionType); impl_wasm_type_info!(BlockBasedDistributionWasm,
BlockBasedDistribution); impl_wasm_type_info!(TimeBasedDistributionWasm,
TimeBasedDistribution); impl_wasm_type_info!(EpochBasedDistributionWasm,
EpochBasedDistribution); which duplicate the existing #[wasm_bindgen(js_class =
...)] impls for RewardDistributionTypeWasm, BlockBasedDistributionWasm,
TimeBasedDistributionWasm, and EpochBasedDistributionWasm; fix this by removing
the redundant macro invocations (or conversely remove the explicit
#[wasm_bindgen] impl blocks) so each type has exactly one impl block—preferably
delete the four impl_wasm_type_info!(...) lines to avoid conflicting
wasm_bindgen attributes.

In `@packages/wasm-dpp2/src/voting/vote_poll.rs`:
- Around line 87-98: Reflect::get does not error for missing properties, so
update the parsing of "contractId" and "indexValues" to explicitly check the
returned JsValue for undefined/null before attempting conversions: after calling
Reflect::get for "contractId" and "indexValues" validate with
JsValue::is_undefined() or is_null() and return a WasmDppError::invalid_argument
with a clear message if missing; only then call
IdentifierWasm::try_from(&js_contract_id) and
js_index_values.with_serde_to_platform_value()/into_array() to avoid downstream
panics and confusing errors.

In `@packages/wasm-sdk/src/queries/protocol.rs`:
- Around line 20-21: The field is_threshold_reached currently serializes as
"thresholdReached" (#[serde(rename = "thresholdReached")]) but the public getter
is named isThresholdReached, causing inconsistent external APIs between
toJSON()/serde output and the getter; pick one approach and make them
consistent: either rename the serde key to "isThresholdReached" or rename the
getter to thresholdReached (or add a documented alias) so both serde (the field
is_threshold_reached) and the public accessor (isThresholdReached or
thresholdReached) produce the same JSON/property name; apply the same fix for
the other occurrences referenced around lines 64–67 to keep naming consistent
across the struct.
🧹 Nitpick comments (25)
packages/wasm-dpp2/src/tokens/status.rs (1)

23-27: Potential JS API break: pausedisPaused rename.

Line 23 renames the JS-visible getter. If backwards compatibility is required, consider keeping a deprecated alias (or ensure all downstream JS/TS usages and docs are updated in this PR).

♻️ Optional compatibility alias
 impl TokenStatusWasm {
     #[wasm_bindgen(getter = "isPaused")]
     pub fn is_paused(&self) -> bool {
         match &self.0 {
             TokenStatus::V0(v0) => v0.paused(),
         }
     }
+
+    // Deprecated alias for compatibility; remove in a future major release.
+    #[wasm_bindgen(getter = "paused")]
+    pub fn paused(&self) -> bool {
+        self.is_paused()
+    }
 }
packages/wasm-dpp2/src/state_transitions/batch/token_transitions/set_price_for_direct_purchase.rs (1)

42-50: Consider handling null in addition to undefined.

The constructor checks price.is_undefined() to determine if a price was provided. In JavaScript, both undefined and null are commonly used to indicate "no value". If a caller passes null, is_undefined() returns false, causing the code to attempt to_wasm conversion on null, which will likely produce a confusing error.

Proposed fix
-        let price: Option<TokenPricingSchedule> = match price.is_undefined() {
-            true => None,
-            false => Some(
+        let price: Option<TokenPricingSchedule> = match price.is_undefined() || price.is_null() {
+            true => None,
+            false => Some(
                 price
                     .to_wasm::<TokenPricingScheduleWasm>("TokenPricingSchedule")?
                     .clone()
                     .into(),
             ),
         };
packages/wasm-sdk/src/queries/mod.rs (1)

303-321: Consider using typed extern types for stricter TypeScript signatures.

The change from JsValue to js_sys::Object improves type safety. However, the relevant code snippets in packages/wasm-dpp2/src/asset_lock_proof/proof.rs show usage of custom typed extern types (e.g., AssetLockProofJSONJs, AssetLockProofObjectJs) which generate more descriptive TypeScript signatures.

If full consistency with wasm-dpp2 patterns is desired, you could define typed extern types like:

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(typescript_type = "ProofMetadataResponseJSON")]
    pub type ProofMetadataResponseJSONJs;
    
    #[wasm_bindgen(typescript_type = "ProofMetadataResponseObject")]
    pub type ProofMetadataResponseObjectJs;
}

That said, using js_sys::Object is a valid intermediate step and the current implementation is functionally correct.

packages/wasm-dpp2/src/tokens/configuration/configuration_convention.rs (1)

96-99: Consider adding input type validation.

Object::from(localizations_value.clone()) will wrap any JsValue (including null, undefined, or primitives) without error. If a non-object value is passed, Object::keys() will return an empty iterator, causing the function to silently return an empty BTreeMap instead of an error.

While the unchecked_param_type attribute provides TypeScript compile-time checking, adding a runtime check would make debugging easier if invalid values slip through:

🔧 Optional: Add type validation
 fn value_to_localizations(
     localizations_value: &JsValue,
 ) -> WasmDppResult<BTreeMap<String, TokenConfigurationLocalization>> {
+    if !localizations_value.is_object() || localizations_value.is_null() {
+        return Err(WasmDppError::invalid_argument(
+            "Localizations must be an object",
+        ));
+    }
     let js_object = Object::from(localizations_value.clone());
     let mut localizations = BTreeMap::new();
packages/wasm-dpp2/src/state_transitions/base/state_transition.rs (1)

340-345: Consider accepting owned JsValue to avoid clone.

The parameter is &JsValue but try_from requires an owned value, necessitating the clone. Accepting purpose: JsValue directly would be more efficient.

♻️ Suggested change
     #[wasm_bindgen(js_name = "getKeyLevelRequirement")]
     pub fn get_key_level_requirement(
         &self,
-        purpose: &JsValue,
+        purpose: JsValue,
     ) -> WasmDppResult<Option<Vec<String>>> {
-        let purpose = PurposeWasm::try_from(purpose.clone())?;
+        let purpose = PurposeWasm::try_from(purpose)?;
packages/wasm-dpp2/src/epoch/finalized_epoch_info.rs (2)

236-238: Consider explicit Map type check for clearer error messages.

Map::from(block_proposers_js) wraps the JsValue without validating it's actually a Map. If a consumer bypasses TypeScript and passes a non-Map value, the error will occur during iteration with a less clear message.

♻️ Suggested improvement
         let block_proposers_js = Reflect::get(&options_obj, &"blockProposers".into())
             .map_err(|_| WasmDppError::invalid_argument("blockProposers is required"))?;
-        let block_proposers = block_proposers_from_map(&Map::from(block_proposers_js))?;
+        let block_proposers_map = block_proposers_js
+            .dyn_into::<Map>()
+            .map_err(|_| WasmDppError::invalid_argument("blockProposers must be a Map"))?;
+        let block_proposers = block_proposers_from_map(&block_proposers_map)?;

382-391: Same Map validation suggestion applies here.

For consistency and clearer error handling, consider using dyn_into::<Map>() as suggested for the constructor.

♻️ Suggested improvement
     pub fn set_block_proposers(
         &mut self,
         block_proposers: BlockProposersMapJs,
     ) -> WasmDppResult<()> {
-        let block_proposers_map =
-            block_proposers_from_map(&Map::from(JsValue::from(block_proposers)))?;
+        let js_map = JsValue::from(block_proposers)
+            .dyn_into::<Map>()
+            .map_err(|_| WasmDppError::invalid_argument("blockProposers must be a Map"))?;
+        let block_proposers_map = block_proposers_from_map(&js_map)?;
         self.v0_mut().block_proposers = block_proposers_map;
         Ok(())
     }
packages/wasm-dpp2/src/tokens/configuration/keeps_history_rules.rs (1)

12-45: Consider rejecting unknown option fields to avoid silent typos.

Right now, misspelled JS option keys are silently ignored and default to false. If you want stricter input validation, consider denying unknown fields (confirm this matches desired JS leniency).

♻️ Optional strict-validation tweak
-#[derive(Deserialize)]
-#[serde(rename_all = "camelCase")]
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
 struct TokenKeepsHistoryRulesOptions {
packages/wasm-dpp2/src/voting/winner_info.rs (2)

7-33: Tighten TS typings for identityId based on kind.

identityId is optional for all kinds, so TS won’t enforce it for WonByIdentity. Consider a discriminated union to align with runtime validation.

♻️ Proposed refactor
-export interface ContestedDocumentVotePollWinnerInfoObject {
-    kind: "NoWinner" | "WonByIdentity" | "Locked";
-    identityId?: Uint8Array;
-}
+export type ContestedDocumentVotePollWinnerInfoObject =
+    | { kind: "NoWinner" | "Locked" }
+    | { kind: "WonByIdentity"; identityId: Uint8Array };
@@
-export interface ContestedDocumentVotePollWinnerInfoJSON {
-    kind: "NoWinner" | "WonByIdentity" | "Locked";
-    identityId?: string;
-}
+export type ContestedDocumentVotePollWinnerInfoJSON =
+    | { kind: "NoWinner" | "Locked" }
+    | { kind: "WonByIdentity"; identityId: string };

53-78: Reject identityId for non-identity kinds to avoid silent misuse.

Right now, identityId is ignored for NoWinner/Locked, which can mask caller mistakes. Consider validating and erroring when it’s provided.

♻️ Suggested validation
-        match kind {
-            "NoWinner" | "noWinner" | "no_winner" | "none" | "NO_WINNER" => {
+        match kind {
+            "NoWinner" | "noWinner" | "no_winner" | "none" | "NO_WINNER" => {
+                if identity_id.is_some() {
+                    return Err(JsValue::from_str(
+                        "identityId must be omitted when kind is 'NoWinner'",
+                    ));
+                }
                 Ok(ContestedDocumentVotePollWinnerInfo::NoWinner.into())
             }
@@
-            "Locked" | "locked" | "LOCKED" => {
+            "Locked" | "locked" | "LOCKED" => {
+                if identity_id.is_some() {
+                    return Err(JsValue::from_str(
+                        "identityId must be omitted when kind is 'Locked'",
+                    ));
+                }
                 Ok(ContestedDocumentVotePollWinnerInfo::Locked.into())
             }
packages/wasm-dpp2/src/tokens/configuration/pre_programmed_distribution.rs (2)

93-138: Validate nested map values before conversion.

If a distribution entry value is not a Map, Map::from(value) can lead to confusing runtime errors. A small guard improves error clarity.

🔧 Proposed guard for nested map values
-        let inner_map = Map::from(value);
+        if !value.is_instance_of::<Map>() {
+            return Err(WasmDppError::invalid_argument(
+                "Distribution amounts must be a Map",
+            ));
+        }
+        let inner_map = Map::from(value);

168-196: Guard against non-Map inputs in constructor/setter.

Map::from uses unchecked casts; adding a fast validation improves error messages and avoids runtime TypeError on invalid inputs.

🔧 Proposed guard for top-level map inputs
-        let distributions_map = distributions_from_map(&Map::from(JsValue::from(distributions)))?;
+        let distributions_value = JsValue::from(distributions);
+        if distributions_value.is_null()
+            || distributions_value.is_undefined()
+            || !distributions_value.is_instance_of::<Map>()
+        {
+            return Err(WasmDppError::invalid_argument(
+                "distributions must be a Map",
+            ));
+        }
+        let distributions_map = distributions_from_map(&Map::from(distributions_value))?;
-        let distributions_map = distributions_from_map(&Map::from(JsValue::from(distributions)))?;
+        let distributions_value = JsValue::from(distributions);
+        if distributions_value.is_null()
+            || distributions_value.is_undefined()
+            || !distributions_value.is_instance_of::<Map>()
+        {
+            return Err(WasmDppError::invalid_argument(
+                "distributions must be a Map",
+            ));
+        }
+        let distributions_map = distributions_from_map(&Map::from(distributions_value))?;
packages/wasm-dpp2/JSVALUE_TYPE_AUDIT.md (1)

3-41: Add concrete recommendations or tracking links to keep this audit actionable.

Each item currently has an empty “Recommendation:” field. Consider adding a short recommendation, owner, or issue link (or mark TODO) so the checklist can be executed.

packages/wasm-dpp2/src/group/token_event.rs (1)

6-32: Consider more specific TypeScript interface definitions for better type safety.

The current interfaces use [key: string]: unknown which is very permissive. Given that TokenEvent is a discriminated union with known variants (Mint, Burn, Freeze, etc.), consider defining variant-specific properties or a discriminated union type for improved TypeScript type checking.

For example:

export type TokenEventObject = 
  | { variant: TokenEventVariant.Mint; amount: bigint; recipient: Uint8Array; ... }
  | { variant: TokenEventVariant.Burn; amount: bigint; ... }
  // ... other variants

However, if the loose typing is intentional for flexibility or due to implementation constraints, this is acceptable.

packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_emergency_action.rs (1)

53-71: Getter/setter type asymmetry may cause confusion in TypeScript.

The emergencyAction getter returns String (line 54-55) while the setter accepts TokenEmergencyActionWasm (line 69). This asymmetry could lead to confusing TypeScript types where reading and writing the same property requires different types.

Consider either:

  1. Having the getter return TokenEmergencyActionWasm for consistency, or
  2. Having the setter accept String (if it can be parsed back to the enum)
♻️ Suggested fix: Return enum type from getter
     #[wasm_bindgen(getter = "emergencyAction")]
-    pub fn get_emergency_action(&self) -> String {
-        TokenEmergencyActionWasm::from(self.0.emergency_action()).into()
+    pub fn get_emergency_action(&self) -> TokenEmergencyActionWasm {
+        TokenEmergencyActionWasm::from(self.0.emergency_action())
     }
packages/wasm-sdk/src/wallet/key_generation.rs (1)

115-135: Consider pre-allocating the vector capacity.

The loop creates key pairs one at a time. Since count is known upfront, pre-allocating would avoid reallocations.

♻️ Suggested improvement
         let network_wasm: NetworkWasm = network.try_into()?;
         let net: Network = network_wasm.into();
 
-        let mut pairs = Vec::new();
+        let mut pairs = Vec::with_capacity(count as usize);
         for _ in 0..count {
             pairs.push(Self::generate_key_pair_internal(net, &network_wasm)?);
         }
packages/wasm-dpp2/src/state_transitions/batch/token_transition.rs (2)

148-185: Consider avoiding unnecessary clone in transition() getter.

The getter uses self.clone().0 which clones the entire wrapper. Since you're only reading and converting the inner value, you could match on &self.0 and clone only the specific variant data being returned.

♻️ Suggested optimization
     #[wasm_bindgen(getter = "transition")]
     pub fn transition(&self) -> JsValue {
-        match self.clone().0 {
+        match &self.0 {
             TokenTransition::Burn(token_transition) => {
-                TokenBurnTransitionWasm::from(token_transition).into()
+                TokenBurnTransitionWasm::from(token_transition.clone()).into()
             }
             // ... similar for other variants
         }
     }

187-221: Same unnecessary clone pattern in transition_type_number() and transition_type().

These getters only read the discriminant and don't need the inner data, so self.clone().0 is wasteful. Matching on &self.0 would avoid the allocation.

♻️ Suggested optimization
     #[wasm_bindgen(getter = "transitionTypeNumber")]
     pub fn transition_type_number(&self) -> u8 {
-        match self.clone().0 {
+        match &self.0 {
             TokenTransition::Burn(_) => 0,
             TokenTransition::Mint(_) => 1,
             // ... etc
         }
     }

     #[wasm_bindgen(getter = "transitionType")]
     pub fn transition_type(&self) -> String {
-        match self.clone().0 {
+        match &self.0 {
             TokenTransition::Burn(_) => "Burn".to_string(),
             // ... etc
         }
     }
packages/wasm-dpp2/src/state_transitions/batch/token_base_transition.rs (1)

163-171: Consider using conventional boolean ordering for readability.

The match arms have inverted ordering (false => Some, true => None) compared to the typical pattern. While correct, consider reordering for consistency with the constructor pattern at lines 78-87.

♻️ Suggested reorder for consistency
         let group_info: Option<GroupStateTransitionInfo> = match using_group_info.is_undefined() {
-            false => Some(
-                using_group_info
-                    .to_wasm::<GroupStateTransitionInfoWasm>("GroupStateTransitionInfo")?
-                    .clone()
-                    .into(),
-            ),
             true => None,
+            false => Some(
+                using_group_info
+                    .to_wasm::<GroupStateTransitionInfoWasm>("GroupStateTransitionInfo")?
+                    .clone()
+                    .into(),
+            ),
         };
packages/wasm-dpp2/src/tokens/configuration/change_control_rules.rs (1)

116-135: Minor: Unnecessary clone in getters.

The getters clone self.0 to match on it, which is functional but slightly inefficient. Consider borrowing instead if the enum supports it.

♻️ Suggested improvement
-    pub fn is_changing_authorized_action_takers_to_no_one_allowed(&self) -> bool {
-        match self.0.clone() {
-            ChangeControlRules::V0(v0) => v0.changing_authorized_action_takers_to_no_one_allowed,
-        }
-    }
+    pub fn is_changing_authorized_action_takers_to_no_one_allowed(&self) -> bool {
+        match &self.0 {
+            ChangeControlRules::V0(v0) => v0.changing_authorized_action_takers_to_no_one_allowed,
+        }
+    }

Apply the same pattern to the other two getters.

packages/wasm-dpp2/src/core/pro_tx_hash.rs (1)

70-86: Consider removing redundant error wrapping.

The error from ProTxHashWasm::try_from(v) is already a WasmDppError. Wrapping it again with WasmDppError::invalid_argument loses the original error details and creates a nested message.

♻️ Suggested simplification
 pub fn pro_tx_hashes_from_js_array(
     array: ProTxHashLikeArrayJs,
 ) -> Result<Vec<ProTxHash>, WasmDppError> {
     let js_value: JsValue = array.into();
     let js_array = js_sys::Array::from(&js_value);
     js_array
         .iter()
-        .map(|v| {
-            ProTxHashWasm::try_from(v)
-                .map(ProTxHash::from)
-                .map_err(|err| {
-                    WasmDppError::invalid_argument(format!("Invalid ProTxHash: {}", err))
-                })
-        })
+        .enumerate()
+        .map(|(i, v)| {
+            ProTxHashWasm::try_from(v)
+                .map(ProTxHash::from)
+                .map_err(|err| {
+                    WasmDppError::invalid_argument(format!("Invalid ProTxHash at index {}: {}", i, err))
+                })
+        })
         .collect()
 }

This adds the array index to the error message for better debugging while still providing context.

packages/wasm-dpp2/src/identifier.rs (1)

339-355: Consider simplifying the error message in identifiers_from_js_array.

The error wrapping adds "Invalid identifier:" but the underlying WasmDppError from IdentifierWasm::try_from likely already contains descriptive context. This may result in redundant messaging.

♻️ Suggested simplification
     js_array
         .iter()
         .map(|v| {
             IdentifierWasm::try_from(v)
                 .map(Identifier::from)
-                .map_err(|err| {
-                    WasmDppError::invalid_argument(format!("Invalid identifier: {}", err))
-                })
         })
         .collect()

Alternatively, keep the wrapper but make it add index context:

.enumerate()
.map(|(i, v)| {
    IdentifierWasm::try_from(v)
        .map(Identifier::from)
        .map_err(|err| WasmDppError::invalid_argument(format!("Invalid identifier at index {}: {}", i, err)))
})
packages/wasm-sdk/src/queries/group.rs (1)

798-835: Consider moving the local import to module level.

The use wasm_dpp2::identifier::identifiers_from_js_array; import is duplicated inside get_groups_data_contracts and get_groups_data_contracts_with_proof_info. Moving it to the top-level imports (line 20) would be cleaner and avoid duplication.

♻️ Suggested refactor
-use wasm_dpp2::identifier::{IdentifierLikeArrayJs, IdentifierLikeJs, IdentifierWasm};
+use wasm_dpp2::identifier::{IdentifierLikeArrayJs, IdentifierLikeJs, IdentifierWasm, identifiers_from_js_array};

Then remove the local use statements from get_groups_data_contracts (line 803) and get_groups_data_contracts_with_proof_info (line 1097).

packages/wasm-dpp2/src/data_contract/model.rs (1)

193-197: Use logical OR (||) instead of bitwise OR (|) for boolean conditions.

The expression js_definitions.is_undefined() | js_definitions.is_null() uses bitwise OR which doesn't short-circuit. While functionally equivalent here since both sides are cheap boolean checks, using || is idiomatic and clearer for boolean logic.

Suggested fix
         let definitions: Option<Value> =
-            match js_definitions.is_undefined() | js_definitions.is_null() {
+            match js_definitions.is_undefined() || js_definitions.is_null() {
                 true => None,
                 false => Some(serialization::platform_value_from_object(js_definitions)?),
             };

Same applies at line 203 for js_tokens.

packages/wasm-dpp2/src/data_contract/document/model.rs (1)

545-566: toJSON doesn't accept a platform_version parameter unlike toObject.

toJSON uses PlatformVersion::latest() at line 548, while toObject accepts a platform_version parameter. For API consistency, consider adding the platform version parameter to toJSON as well.

shumkov and others added 4 commits January 27, 2026 13:10
Change parameter js_name from 'binaryData' to 'signatureBytes' to avoid
confusion with set_data's parameter in TypeScript.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/wasm-dpp2/src/identity/transitions/public_key_in_creation.rs`:
- Around line 227-232: Update the TypeScript declaration for
IdentityPublicKeyInCreationOptions so its data field is a string (hex encoded)
instead of Uint8Array to match how IdentityPublicKeyWasm::constructor is called;
locate the interface/type named IdentityPublicKeyInCreationOptions and change
the type of data to string with a comment like "// hex encoded", mirroring the
existing IdentityPublicKeyOptions in public_key.rs, and ensure any related
usages or JSDoc reflect the string/hex format.

shumkov and others added 6 commits January 27, 2026 17:35
Use try_from_options directly with options.as_ref() instead of converting
to Object first. Implement TryFrom<&JsValue> for ContractBoundsWasm to
enable using try_from_options_optional for contractBounds extraction.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Accept bytes directly instead of hex-encoded string for the data field.
serde_wasm_bindgen handles Uint8Array to Vec<u8> conversion automatically.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Upgrade from ESLint 8.57.0 to ESLint 9.x with flat config format
- Replace eslint-config-airbnb-base with eslint-config-airbnb-extended v3.0.1
- Create shared configs in eslint/ directory (base, typescript, wasm, mocha-tests)
- Migrate all 24 packages to eslint.config.mjs flat config format
- Delete all legacy .eslintrc* and .eslintignore files
- Update inline eslint-disable comments (import/ to import-x/)
- Fix code issues: unnecessary escapes in wasm-sdk, hex conversion in wasm-dpp2

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Update all packages to use explicit version constraints:

- ESLint: * → ^9.18.0 (21 packages)

- TypeScript: ^5.3.3 → ^5.7.3 (5 packages)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Disable @stylistic/no-mixed-operators rule in base config

- Add TypeScript support to wasm-dpp eslint config

- Ignore generated protobuf code in dapi-grpc (clients/, src/)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/dapi-grpc/eslint.config.mjs`:
- Around line 8-9: Update the misleading comment above the ignores array:
instead of saying "Generated protobuf code - ignore clients and src generated
directories", change it to accurately reflect what is being ignored by the rule
(e.g., "Ignore build artifacts, generated protobuf code, client bundles and
hand-written Rust sources (src/)") so it references the actual ignores array
(ignores: ['dist/**', 'node_modules/**', 'clients/**', 'src/**']) and clarifies
that src/** contains handwritten Rust files rather than generated protobuf code.
🧹 Nitpick comments (1)
packages/dashmate/eslint.config.mjs (1)

14-16: Consider placing global ignores at the start of the config array.

In ESLint 9 flat config, placing an ignores-only object at the end of the array creates a configuration-specific ignore pattern rather than a global ignore. For truly global ignores that apply before any other processing, it's recommended to place them at the beginning of the array.

💡 Suggested reordering
 export default [
+  {
+    ignores: ['dist/**', 'node_modules/**'],
+  },
   ...baseConfig,
   {
     files: ['**/*.js'],
     rules: {
       // Require .js extension for imports (ESM)
       'import-x/extensions': ['error', 'ignorePackages', { js: 'always' }],
     },
   },
   mochaTestConfig,
-  {
-    ignores: ['dist/**', 'node_modules/**'],
-  },
 ];

shumkov and others added 3 commits January 28, 2026 12:48
Addresses moderate severity deprecation warning in tar 7.5.x

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/dashmate/package.json`:
- Line 96: The package.json entry for the tar dependency uses a non-existent
version specifier "^7.6.0"; open packages/dashmate/package.json, locate the
"tar" dependency line and change the version to a valid published version (e.g.,
"^7.5.7" or the previously used "^7.5.4") so the constraint is satisfiable and
installs correctly, then update lockfile by running your package manager
(yarn/npm) to regenerate yarn.lock or package-lock.json.

shumkov and others added 6 commits January 28, 2026 13:36
… data

Update test fixture to pass Uint8Array instead of hex string for the
data field, matching the updated IdentityPublicKey constructor API.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Pin tar to exact version 7.5.7 in dashmate

- Add tar resolution in root package.json

- Addresses moderate severity advisory for tar 7.5.6

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Ensure options is properly converted to JsValue before passing to
try_from_options functions to avoid Reflect.get on non-object errors.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The lint script was accidentally emptied during the ESLint 9 migration.
This restores it and adds eslint as a devDependency. Also fixes lint
errors: adds missing Identity/Identifier imports, removes unused
eslint-disable directives, and fixes unused catch variable.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…ions

Refactored 4 transition constructors to use serde for deserializing
primitive fields (u16, u32, u64, Vec<u8>) instead of manual extraction.
Complex types like IdentifierLike are still handled manually.

This follows the pattern: serde for 3+ primitives, manual for *LikeJs types.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…ault handling

Move undefined/null check into TryFrom<&JsValue> implementation to return default value, eliminating verbose 5-line patterns at 17+ call sites.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@thephez thephez changed the title refactor(sdk)!: comprehansive Evo SDK refactroring refactor(sdk)!: comprehensive Evo SDK refactoring Jan 28, 2026
shumkov and others added 7 commits January 29, 2026 09:12
…ions

Move IdentifierWasm fields into serde input structs, leveraging its
custom Deserialize implementation to simplify constructors.

Files updated:
- identity transitions: credit_transfer, credit_withdrawal, update, masternode_vote
- data_contract/document/model.rs
- epoch/extended_epoch_info.rs
- state_transitions/batch/token_payment_info.rs

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Replace Object::from with try_to_object for validating user input
to ensure proper error handling when non-object values are passed.
Also update try_to_* utility functions to accept impl Into<JsValue>
for more flexible API.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
….clone()

Reorder constructor operations to extract complex types first (borrowing &options),
then call serde_wasm_bindgen::from_value(options.into()) last (consuming options).
This eliminates unnecessary .clone() calls on options objects since try_from_options
borrows while serde consumes.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…tead of serde

Remove IdentifierWasm and other wasm_bindgen types from serde input
structs since serde deserialization cannot properly handle already-
constructed wasm_bindgen instances passed from JavaScript.

Pattern applied: extract complex types first via try_from_options
(borrows &options), then deserialize primitive fields last via serde
(consumes options).

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…e conversions

Replace verbose `to_wasm::<TypeWasm>("TypeName")?.clone()` patterns with
cleaner `TypeWasm::try_from(&value)?` by adding `impl_try_from_js_value!`
macro to wasm types and simplifying constructors to use `try_from_options`
and `try_from_options_optional` instead of `_with` variants.

- Add impl_try_from_js_value! to 20+ wasm types
- Simplify constructors using direct try_from_options calls
- Remove unused IntoWasm imports throughout
- Net reduction of ~140 lines of code

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…nput types

Replace inline `Type | string | number` patterns with centralized *Like
type aliases for consistent TypeScript interfaces:

- Add TokenDistributionTypeLike and TokenDistributionTypeLikeJs
- Add ActionGoalLike and ActionGoalLikeJs
- Update DataContractOptions to use PlatformVersionLike
- Update TokenPaymentInfoOptions to use GasFeesPaidByLike
- Update CanChangeAdminActionTakersOptions to use ActionGoalLike
- Update TokenClaimTransition to use TokenDistributionTypeLike
- Update IdentityPublicKeyInCreationOptions to use PurposeLike,
  SecurityLevelLike, KeyTypeLike

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Replace manual TryFrom and try_from_options implementations with
impl_try_from_js_value! and impl_try_from_options! macros:

- IdentitySignerWasm: use macros, remove 24 lines of boilerplate
- PlatformAddressSignerWasm: use macros, remove 24 lines of boilerplate
- PlatformAddressInput/Output: use try_to_u64 instead of local helper
- Update 29 call sites in wasm-sdk to use new method signature

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants