Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c5325c9
feat(wasm-dpp2,js-sdk): add shielded pool WASM bindings and JS SDK me…
QuantumExplorer Mar 12, 2026
65c02a6
revert: remove js-dash-sdk and wasm-dpp changes from shielded PR
QuantumExplorer Mar 12, 2026
8b71f68
feat(wasm-sdk): add shielded pool query methods
QuantumExplorer Mar 12, 2026
3256346
feat(wasm-dpp2): implement shielded proof result wrappers
QuantumExplorer Mar 12, 2026
d08e087
fix(wasm-sdk): correct computePlatformSighash doc for extraData format
QuantumExplorer Mar 12, 2026
98a20b6
fix(wasm-sdk): remove needless borrow in computePlatformSighash
QuantumExplorer Mar 12, 2026
5d4ee20
fix(wasm): address PR review feedback for shielded bindings
QuantumExplorer Mar 13, 2026
35b7ba4
fix(wasm-sdk): reject nullifiers that are not exactly 32 bytes
QuantumExplorer Mar 13, 2026
a87c6ad
Merge branch 'v3.1-dev' into feat/zk-wasm-js-bindings
QuantumExplorer Mar 13, 2026
0b48cd3
Merge branch 'v3.1-dev' into feat/zk-wasm-js-bindings
QuantumExplorer Mar 16, 2026
74e9290
feat(wasm): address all review feedback on shielded WASM bindings
QuantumExplorer Mar 16, 2026
7f216ec
feat(wasm): improve TypeScript interface definitions per shumkov review
QuantumExplorer Mar 16, 2026
c6a73b7
fix(wasm): use base64 string for all byte fields in OrchardActionJSON
QuantumExplorer Mar 16, 2026
4e29ccd
fix(wasm): use number | string for BigInt fields in JSON interfaces
QuantumExplorer Mar 16, 2026
fd850e1
fix(wasm): use typed parameters in shielded transition constructors
QuantumExplorer Mar 16, 2026
2db3796
fix(wasm): correct TS types to match actual serde output
QuantumExplorer Mar 16, 2026
c55a943
Merge branch 'v3.1-dev' into feat/zk-wasm-js-bindings
QuantumExplorer Mar 17, 2026
c6a2b3f
fix(wasm): change $version to $formatVersion, add Options constructor
QuantumExplorer Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/wasm-dpp2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod mock_bls;
pub mod platform_address;
pub mod public_key;
pub mod serialization;
pub mod shielded;
pub mod state_transitions;
pub mod tokens;
pub mod utils;
Expand Down Expand Up @@ -66,6 +67,10 @@ pub use platform_address::{
PlatformAddressWasm, default_fee_strategy, fee_strategy_from_steps,
fee_strategy_from_steps_or_default, outputs_to_btree_map, outputs_to_optional_btree_map,
};
pub use shielded::{
ShieldFromAssetLockTransitionWasm, ShieldTransitionWasm, ShieldedTransferTransitionWasm,
ShieldedWithdrawalTransitionWasm, UnshieldTransitionWasm,
};
pub use state_transitions::base::{GroupStateTransitionInfoWasm, StateTransitionWasm};
pub use state_transitions::proof_result::{StateTransitionProofResultTypeJs, convert_proof_result};
pub use tokens::*;
Expand Down
37 changes: 37 additions & 0 deletions packages/wasm-dpp2/src/shielded/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
pub mod shield_from_asset_lock_transition;
pub mod shield_transition;
pub mod shielded_transfer_transition;
pub mod shielded_withdrawal_transition;
pub mod unshield_transition;

pub use shield_from_asset_lock_transition::ShieldFromAssetLockTransitionWasm;
pub use shield_transition::ShieldTransitionWasm;
pub use shielded_transfer_transition::ShieldedTransferTransitionWasm;
pub use shielded_withdrawal_transition::ShieldedWithdrawalTransitionWasm;
pub use unshield_transition::UnshieldTransitionWasm;

use crate::error::WasmDppResult;
use wasm_bindgen::prelude::wasm_bindgen;

/// Compute the platform sighash from an Orchard bundle commitment and extra data.
///
/// `sighash = SHA-256("DashPlatformSighash" || bundleCommitment || extraData)`
///
/// - For shield and shielded_transfer transitions, `extraData` should be empty.
/// - For unshield transitions, `extraData` = serialized `outputAddress` bytes.
/// - For shielded withdrawal transitions, `extraData` = `outputScript` bytes.
#[wasm_bindgen(js_name = computePlatformSighash)]
pub fn compute_platform_sighash_wasm(
bundle_commitment: &[u8],
extra_data: &[u8],
) -> WasmDppResult<Vec<u8>> {
if bundle_commitment.len() != 32 {
return Err(crate::error::WasmDppError::invalid_argument(format!(
"bundleCommitment must be exactly 32 bytes, got {}",
bundle_commitment.len()
)));
}
let commitment: &[u8; 32] = bundle_commitment.try_into().expect("checked length above");
let result = dpp::shielded::compute_platform_sighash(commitment, extra_data);
Ok(result.to_vec())
}
271 changes: 271 additions & 0 deletions packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
use crate::asset_lock_proof::AssetLockProofWasm;
use crate::error::{WasmDppError, WasmDppResult};
use crate::identifier::IdentifierWasm;
use crate::utils::try_from_options;
use crate::{impl_wasm_conversions_serde, impl_wasm_type_info};
use dpp::platform_value::BinaryData;
use dpp::serialization::{PlatformDeserializable, PlatformSerializable};
use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition;
use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0;
use dpp::state_transition::{StateTransition, StateTransitionLike};
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ShieldFromAssetLockTransitionSimpleFields {
#[serde(default)]
actions: Vec<dpp::shielded::SerializedAction>,
#[serde(default)]
value_balance: u64,
#[serde(default)]
anchor: Vec<u8>,
#[serde(default)]
proof: Vec<u8>,
#[serde(default)]
binding_signature: Vec<u8>,
#[serde(default)]
signature: Vec<u8>,
}
Comment on lines +14 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: #[serde(default)] on all fields silently accepts missing required cryptographic fields

Every field in ShieldFromAssetLockTransitionSimpleFields has #[serde(default)], which means the constructor silently succeeds with empty vectors and zero values if the JS caller omits actions, proof, bindingSignature, etc. While anchor and binding_signature are validated for correct length downstream (32 and 64 bytes respectively), the other fields — actions, proof, value_balance, signature — will silently default to empty/zero.

This creates a confusing failure mode: instead of getting a clear "missing field" error at the WASM boundary, a JS caller omitting required fields gets a structurally valid but logically broken transition that will fail much later during validation.

Consider removing #[serde(default)] from fields that are truly required (actions, proof, binding_signature, signature), or at minimum adding explicit emptiness checks after deserialization.

source: ['claude', 'codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs`:
- [SUGGESTION] lines 14-29: `#[serde(default)]` on all fields silently accepts missing required cryptographic fields
  Every field in `ShieldFromAssetLockTransitionSimpleFields` has `#[serde(default)]`, which means the constructor silently succeeds with empty vectors and zero values if the JS caller omits `actions`, `proof`, `bindingSignature`, etc. While `anchor` and `binding_signature` are validated for correct length downstream (32 and 64 bytes respectively), the other fields — `actions`, `proof`, `value_balance`, `signature` — will silently default to empty/zero.

This creates a confusing failure mode: instead of getting a clear "missing field" error at the WASM boundary, a JS caller omitting required fields gets a structurally valid but logically broken transition that will fail much later during validation.

Consider removing `#[serde(default)]` from fields that are truly required (actions, proof, binding_signature, signature), or at minimum adding explicit emptiness checks after deserialization.


#[wasm_bindgen(typescript_custom_section)]
const TS_TYPES: &str = r#"
/**
* A serialized Orchard action (spend-output pair) in binary/Object form.
*/
export interface SerializedOrchardAction {
nullifier: Uint8Array;
rk: Uint8Array;
cmx: Uint8Array;
encryptedNote: Uint8Array;
cvNet: Uint8Array;
spendAuthSig: Uint8Array;
}

/**
* A serialized Orchard action (spend-output pair) in JSON form.
*/
export interface SerializedOrchardActionJSON {
nullifier: string;
rk: string;
cmx: string;
encryptedNote: string;
cvNet: string;
spendAuthSig: string;
}

/**
* Options for constructing a ShieldFromAssetLockTransition.
* Uses WASM instance types for complex fields like AssetLockProof.
*/
export interface ShieldFromAssetLockTransitionOptions {
assetLockProof: AssetLockProof;
actions: SerializedOrchardAction[];
valueBalance: bigint;
anchor: Uint8Array;
proof: Uint8Array;
bindingSignature: Uint8Array;
signature: Uint8Array;
}

/**
* ShieldFromAssetLockTransition serialized as a plain object.
*/
export interface ShieldFromAssetLockTransitionObject {
$formatVersion: string;
assetLockProof: AssetLockProofObject;
actions: SerializedOrchardAction[];
valueBalance: bigint;
anchor: Uint8Array;
proof: Uint8Array;
bindingSignature: Uint8Array;
signature: Uint8Array;
}

/**
* ShieldFromAssetLockTransition serialized as JSON (human-readable).
*/
export interface ShieldFromAssetLockTransitionJSON {
$formatVersion: string;
assetLockProof: AssetLockProofJSON;
actions: SerializedOrchardActionJSON[];
valueBalance: number | string;
anchor: string;
proof: string;
bindingSignature: string;
signature: string;
}
"#;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "ShieldFromAssetLockTransitionOptions")]
pub type ShieldFromAssetLockTransitionOptionsJs;

#[wasm_bindgen(typescript_type = "ShieldFromAssetLockTransitionObject")]
pub type ShieldFromAssetLockTransitionObjectJs;

#[wasm_bindgen(typescript_type = "ShieldFromAssetLockTransitionJSON")]
pub type ShieldFromAssetLockTransitionJSONJs;
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(transparent)]
#[wasm_bindgen(js_name = ShieldFromAssetLockTransition)]
pub struct ShieldFromAssetLockTransitionWasm(ShieldFromAssetLockTransition);

impl From<ShieldFromAssetLockTransition> for ShieldFromAssetLockTransitionWasm {
fn from(v: ShieldFromAssetLockTransition) -> Self {
ShieldFromAssetLockTransitionWasm(v)
}
}

impl From<ShieldFromAssetLockTransitionWasm> for ShieldFromAssetLockTransition {
fn from(v: ShieldFromAssetLockTransitionWasm) -> Self {
v.0
}
}

#[wasm_bindgen(js_class = ShieldFromAssetLockTransition)]
impl ShieldFromAssetLockTransitionWasm {
#[wasm_bindgen(constructor)]
pub fn new(
options: ShieldFromAssetLockTransitionOptionsJs,
) -> WasmDppResult<ShieldFromAssetLockTransitionWasm> {
// Extract assetLockProof as a WASM instance (required)
let asset_lock: AssetLockProofWasm = try_from_options(&options, "assetLockProof")?;

// Extract remaining simple fields via serde
let fields: ShieldFromAssetLockTransitionSimpleFields =
serde_wasm_bindgen::from_value(options.into())
.map_err(|e| WasmDppError::serialization(e.to_string()))?;

let anchor: [u8; 32] = fields
.anchor
.try_into()
.map_err(|_| WasmDppError::invalid_argument("anchor must be exactly 32 bytes"))?;

let binding_signature: [u8; 64] = fields.binding_signature.try_into().map_err(|_| {
WasmDppError::invalid_argument("bindingSignature must be exactly 64 bytes")
})?;

Ok(ShieldFromAssetLockTransitionWasm(
ShieldFromAssetLockTransition::V0(ShieldFromAssetLockTransitionV0 {
asset_lock_proof: asset_lock.into(),
actions: fields.actions,
value_balance: fields.value_balance,
anchor,
proof: fields.proof,
binding_signature,
signature: BinaryData::from(fields.signature),
}),
))
}

#[wasm_bindgen(js_name = getType)]
pub fn get_type(&self) -> u8 {
self.0.state_transition_type() as u8
}

/// Returns the asset lock proof as a JS value.
#[wasm_bindgen(js_name = getAssetLockProof)]
pub fn get_asset_lock_proof(&self) -> WasmDppResult<JsValue> {
let proof = match &self.0 {
ShieldFromAssetLockTransition::V0(v0) => &v0.asset_lock_proof,
};
serde_wasm_bindgen::to_value(proof).map_err(|e| WasmDppError::serialization(e.to_string()))
}

/// Returns the serialized Orchard actions as a JS array.
#[wasm_bindgen(js_name = getActions)]
pub fn get_actions(&self) -> WasmDppResult<JsValue> {
let inner = match &self.0 {
ShieldFromAssetLockTransition::V0(v0) => &v0.actions,
};
serde_wasm_bindgen::to_value(inner).map_err(|e| WasmDppError::serialization(e.to_string()))
}

/// Returns the net value balance.
#[wasm_bindgen(js_name = getValueBalance)]
pub fn get_value_balance(&self) -> u64 {
match &self.0 {
ShieldFromAssetLockTransition::V0(v0) => v0.value_balance,
}
}

/// Returns the anchor (32-byte Merkle root).
#[wasm_bindgen(js_name = getAnchor)]
pub fn get_anchor(&self) -> Vec<u8> {
match &self.0 {
ShieldFromAssetLockTransition::V0(v0) => v0.anchor.to_vec(),
}
}

/// Returns the Halo2 proof bytes.
#[wasm_bindgen(js_name = getProof)]
pub fn get_proof(&self) -> Vec<u8> {
match &self.0 {
ShieldFromAssetLockTransition::V0(v0) => v0.proof.clone(),
}
}

/// Returns the RedPallas binding signature (64 bytes).
#[wasm_bindgen(js_name = getBindingSignature)]
pub fn get_binding_signature(&self) -> Vec<u8> {
match &self.0 {
ShieldFromAssetLockTransition::V0(v0) => v0.binding_signature.to_vec(),
}
}

/// Returns the ECDSA signature.
#[wasm_bindgen(js_name = getSignature)]
pub fn get_signature(&self) -> Vec<u8> {
match &self.0 {
ShieldFromAssetLockTransition::V0(v0) => v0.signature.to_vec(),
}
}

#[wasm_bindgen(js_name = getModifiedDataIds)]
pub fn modified_data_ids(&self) -> Vec<IdentifierWasm> {
self.0
.modified_data_ids()
.into_iter()
.map(IdentifierWasm::from)
.collect()
}

#[wasm_bindgen(js_name = toBytes)]
pub fn to_bytes(&self) -> WasmDppResult<Vec<u8>> {
Ok(PlatformSerializable::serialize_to_bytes(
&StateTransition::ShieldFromAssetLock(self.0.clone()),
)?)
}

#[wasm_bindgen(js_name = fromBytes)]
pub fn from_bytes(bytes: Vec<u8>) -> WasmDppResult<ShieldFromAssetLockTransitionWasm> {
let st = StateTransition::deserialize_from_bytes(&bytes)?;
match st {
StateTransition::ShieldFromAssetLock(inner) => Ok(inner.into()),
_ => Err(WasmDppError::invalid_argument(
"Invalid state transition type: expected ShieldFromAssetLock",
)),
}
}

#[wasm_bindgen(js_name = toStateTransition)]
pub fn to_state_transition(&self) -> crate::state_transitions::base::StateTransitionWasm {
StateTransition::ShieldFromAssetLock(self.0.clone()).into()
}
}

impl_wasm_conversions_serde!(
ShieldFromAssetLockTransitionWasm,
ShieldFromAssetLockTransition,
ShieldFromAssetLockTransitionObjectJs,
ShieldFromAssetLockTransitionJSONJs
);

impl_wasm_type_info!(
ShieldFromAssetLockTransitionWasm,
ShieldFromAssetLockTransition
);
Loading
Loading