diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index 8ff7050bff0..ad00d08c620 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -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; @@ -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::*; diff --git a/packages/wasm-dpp2/src/shielded/mod.rs b/packages/wasm-dpp2/src/shielded/mod.rs new file mode 100644 index 00000000000..ef9bce0fe42 --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/mod.rs @@ -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> { + 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()) +} diff --git a/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs b/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs new file mode 100644 index 00000000000..5b47c5d354b --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs @@ -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, + #[serde(default)] + value_balance: u64, + #[serde(default)] + anchor: Vec, + #[serde(default)] + proof: Vec, + #[serde(default)] + binding_signature: Vec, + #[serde(default)] + signature: Vec, +} + +#[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 for ShieldFromAssetLockTransitionWasm { + fn from(v: ShieldFromAssetLockTransition) -> Self { + ShieldFromAssetLockTransitionWasm(v) + } +} + +impl From 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + match &self.0 { + ShieldFromAssetLockTransition::V0(v0) => v0.signature.to_vec(), + } + } + + #[wasm_bindgen(js_name = getModifiedDataIds)] + pub fn modified_data_ids(&self) -> Vec { + self.0 + .modified_data_ids() + .into_iter() + .map(IdentifierWasm::from) + .collect() + } + + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> WasmDppResult> { + Ok(PlatformSerializable::serialize_to_bytes( + &StateTransition::ShieldFromAssetLock(self.0.clone()), + )?) + } + + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes(bytes: Vec) -> WasmDppResult { + 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 +); diff --git a/packages/wasm-dpp2/src/shielded/shield_transition.rs b/packages/wasm-dpp2/src/shielded/shield_transition.rs new file mode 100644 index 00000000000..351e0b1f452 --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/shield_transition.rs @@ -0,0 +1,255 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; +use dpp::state_transition::shield_transition::ShieldTransition; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * Fee strategy step: defines how fees are paid from inputs or outputs. + * Externally-tagged enum: exactly one key present per object. + */ +export type FeeStrategyStepObject = + | { deductFromInput: number } + | { reduceOutput: number }; + +/** + * Address witness for P2PKH spending. + */ +export interface AddressWitnessP2pkhObject { + type: "p2pkh"; + signature: Uint8Array; +} + +/** + * Address witness for P2SH spending. + */ +export interface AddressWitnessP2shObject { + type: "p2sh"; + signatures: Uint8Array[]; + redeemScript: Uint8Array; +} + +/** + * Address witness (P2PKH or P2SH) in Object form. + */ +export type AddressWitnessObject = AddressWitnessP2pkhObject | AddressWitnessP2shObject; + +/** + * Address witness for P2PKH spending (JSON form). + */ +export interface AddressWitnessP2pkhJSON { + type: "p2pkh"; + signature: string; +} + +/** + * Address witness for P2SH spending (JSON form). + */ +export interface AddressWitnessP2shJSON { + type: "p2sh"; + signatures: string[]; + redeemScript: string; +} + +/** + * Address witness (P2PKH or P2SH) in JSON form. + */ +export type AddressWitnessJSON = AddressWitnessP2pkhJSON | AddressWitnessP2shJSON; + +/** + * ShieldTransition serialized as a plain object. + */ +export interface ShieldTransitionObject { + $formatVersion: string; + inputs: Record; + actions: SerializedOrchardAction[]; + amount: bigint; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; + feeStrategy: FeeStrategyStepObject[]; + userFeeIncrease: number; + inputWitnesses: AddressWitnessObject[]; +} + +/** + * ShieldTransition serialized as JSON (human-readable). + */ +export interface ShieldTransitionJSON { + $formatVersion: string; + inputs: Record; + actions: SerializedOrchardActionJSON[]; + amount: number | string; + anchor: string; + proof: string; + bindingSignature: string; + feeStrategy: FeeStrategyStepObject[]; + userFeeIncrease: number; + inputWitnesses: AddressWitnessJSON[]; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ShieldTransitionObject")] + pub type ShieldTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "ShieldTransitionJSON")] + pub type ShieldTransitionJSONJs; +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(transparent)] +#[wasm_bindgen(js_name = ShieldTransition)] +pub struct ShieldTransitionWasm(ShieldTransition); + +impl From for ShieldTransitionWasm { + fn from(v: ShieldTransition) -> Self { + ShieldTransitionWasm(v) + } +} + +impl From for ShieldTransition { + fn from(v: ShieldTransitionWasm) -> Self { + v.0 + } +} + +#[wasm_bindgen(js_class = ShieldTransition)] +impl ShieldTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn new(value: ShieldTransitionObjectJs) -> WasmDppResult { + let inner: ShieldTransition = serde_wasm_bindgen::from_value(value.into()) + .map_err(|e| WasmDppError::serialization(e.to_string()))?; + Ok(ShieldTransitionWasm(inner)) + } + + #[wasm_bindgen(js_name = getType)] + pub fn get_type(&self) -> u8 { + self.0.state_transition_type() as u8 + } + + /// Returns the inputs map as a JS object. + #[wasm_bindgen(js_name = getInputs)] + pub fn get_inputs(&self) -> WasmDppResult { + let inner = match &self.0 { + ShieldTransition::V0(v0) => &v0.inputs, + }; + serde_wasm_bindgen::to_value(inner).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 { + let inner = match &self.0 { + ShieldTransition::V0(v0) => &v0.actions, + }; + serde_wasm_bindgen::to_value(inner).map_err(|e| WasmDppError::serialization(e.to_string())) + } + + /// Returns the shield amount (credits entering the pool). + #[wasm_bindgen(js_name = getAmount)] + pub fn get_amount(&self) -> u64 { + match &self.0 { + ShieldTransition::V0(v0) => v0.amount, + } + } + + /// Returns the anchor (32-byte Merkle root). + #[wasm_bindgen(js_name = getAnchor)] + pub fn get_anchor(&self) -> Vec { + match &self.0 { + ShieldTransition::V0(v0) => v0.anchor.to_vec(), + } + } + + /// Returns the Halo2 proof bytes. + #[wasm_bindgen(js_name = getProof)] + pub fn get_proof(&self) -> Vec { + match &self.0 { + ShieldTransition::V0(v0) => v0.proof.clone(), + } + } + + /// Returns the RedPallas binding signature (64 bytes). + #[wasm_bindgen(js_name = getBindingSignature)] + pub fn get_binding_signature(&self) -> Vec { + match &self.0 { + ShieldTransition::V0(v0) => v0.binding_signature.to_vec(), + } + } + + /// Returns the fee strategy as a JS value. + #[wasm_bindgen(js_name = getFeeStrategy)] + pub fn get_fee_strategy(&self) -> WasmDppResult { + let strategy = match &self.0 { + ShieldTransition::V0(v0) => &v0.fee_strategy, + }; + serde_wasm_bindgen::to_value(strategy) + .map_err(|e| WasmDppError::serialization(e.to_string())) + } + + /// Returns the user fee increase multiplier. + #[wasm_bindgen(js_name = getUserFeeIncrease)] + pub fn get_user_fee_increase(&self) -> u16 { + match &self.0 { + ShieldTransition::V0(v0) => v0.user_fee_increase, + } + } + + /// Returns the input witnesses as a JS value. + #[wasm_bindgen(js_name = getInputWitnesses)] + pub fn get_input_witnesses(&self) -> WasmDppResult { + let witnesses = match &self.0 { + ShieldTransition::V0(v0) => &v0.input_witnesses, + }; + serde_wasm_bindgen::to_value(witnesses) + .map_err(|e| WasmDppError::serialization(e.to_string())) + } + + #[wasm_bindgen(js_name = getModifiedDataIds)] + pub fn modified_data_ids(&self) -> Vec { + self.0 + .modified_data_ids() + .into_iter() + .map(IdentifierWasm::from) + .collect() + } + + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> WasmDppResult> { + Ok(PlatformSerializable::serialize_to_bytes( + &StateTransition::Shield(self.0.clone()), + )?) + } + + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes(bytes: Vec) -> WasmDppResult { + let st = StateTransition::deserialize_from_bytes(&bytes)?; + match st { + StateTransition::Shield(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected Shield", + )), + } + } + + #[wasm_bindgen(js_name = toStateTransition)] + pub fn to_state_transition(&self) -> crate::state_transitions::base::StateTransitionWasm { + StateTransition::Shield(self.0.clone()).into() + } +} + +impl_wasm_conversions_serde!( + ShieldTransitionWasm, + ShieldTransition, + ShieldTransitionObjectJs, + ShieldTransitionJSONJs +); + +impl_wasm_type_info!(ShieldTransitionWasm, ShieldTransition); diff --git a/packages/wasm-dpp2/src/shielded/shielded_transfer_transition.rs b/packages/wasm-dpp2/src/shielded/shielded_transfer_transition.rs new file mode 100644 index 00000000000..0b54bb4ae58 --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/shielded_transfer_transition.rs @@ -0,0 +1,160 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; +use dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * ShieldedTransferTransition serialized as a plain object. + */ +export interface ShieldedTransferTransitionObject { + $formatVersion: string; + actions: SerializedOrchardAction[]; + valueBalance: bigint; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} + +/** + * ShieldedTransferTransition serialized as JSON (human-readable). + */ +export interface ShieldedTransferTransitionJSON { + $formatVersion: string; + actions: SerializedOrchardActionJSON[]; + valueBalance: number | string; + anchor: string; + proof: string; + bindingSignature: string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ShieldedTransferTransitionObject")] + pub type ShieldedTransferTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "ShieldedTransferTransitionJSON")] + pub type ShieldedTransferTransitionJSONJs; +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(transparent)] +#[wasm_bindgen(js_name = ShieldedTransferTransition)] +pub struct ShieldedTransferTransitionWasm(ShieldedTransferTransition); + +impl From for ShieldedTransferTransitionWasm { + fn from(v: ShieldedTransferTransition) -> Self { + ShieldedTransferTransitionWasm(v) + } +} + +impl From for ShieldedTransferTransition { + fn from(v: ShieldedTransferTransitionWasm) -> Self { + v.0 + } +} + +#[wasm_bindgen(js_class = ShieldedTransferTransition)] +impl ShieldedTransferTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn new( + value: ShieldedTransferTransitionObjectJs, + ) -> WasmDppResult { + let inner: ShieldedTransferTransition = serde_wasm_bindgen::from_value(value.into()) + .map_err(|e| WasmDppError::serialization(e.to_string()))?; + Ok(ShieldedTransferTransitionWasm(inner)) + } + + #[wasm_bindgen(js_name = getType)] + pub fn get_type(&self) -> u8 { + self.0.state_transition_type() as u8 + } + + /// Returns the serialized Orchard actions as a JS array. + #[wasm_bindgen(js_name = getActions)] + pub fn get_actions(&self) -> WasmDppResult { + let inner = match &self.0 { + ShieldedTransferTransition::V0(v0) => &v0.actions, + }; + serde_wasm_bindgen::to_value(inner).map_err(|e| WasmDppError::serialization(e.to_string())) + } + + /// Returns the value balance (fee amount leaving the pool). + #[wasm_bindgen(js_name = getValueBalance)] + pub fn get_value_balance(&self) -> u64 { + match &self.0 { + ShieldedTransferTransition::V0(v0) => v0.value_balance, + } + } + + /// Returns the anchor (32-byte Merkle root). + #[wasm_bindgen(js_name = getAnchor)] + pub fn get_anchor(&self) -> Vec { + match &self.0 { + ShieldedTransferTransition::V0(v0) => v0.anchor.to_vec(), + } + } + + /// Returns the Halo2 proof bytes. + #[wasm_bindgen(js_name = getProof)] + pub fn get_proof(&self) -> Vec { + match &self.0 { + ShieldedTransferTransition::V0(v0) => v0.proof.clone(), + } + } + + /// Returns the RedPallas binding signature (64 bytes). + #[wasm_bindgen(js_name = getBindingSignature)] + pub fn get_binding_signature(&self) -> Vec { + match &self.0 { + ShieldedTransferTransition::V0(v0) => v0.binding_signature.to_vec(), + } + } + + #[wasm_bindgen(js_name = getModifiedDataIds)] + pub fn modified_data_ids(&self) -> Vec { + self.0 + .modified_data_ids() + .into_iter() + .map(IdentifierWasm::from) + .collect() + } + + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> WasmDppResult> { + Ok(PlatformSerializable::serialize_to_bytes( + &StateTransition::ShieldedTransfer(self.0.clone()), + )?) + } + + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes(bytes: Vec) -> WasmDppResult { + let st = StateTransition::deserialize_from_bytes(&bytes)?; + match st { + StateTransition::ShieldedTransfer(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected ShieldedTransfer", + )), + } + } + + #[wasm_bindgen(js_name = toStateTransition)] + pub fn to_state_transition(&self) -> crate::state_transitions::base::StateTransitionWasm { + StateTransition::ShieldedTransfer(self.0.clone()).into() + } +} + +impl_wasm_conversions_serde!( + ShieldedTransferTransitionWasm, + ShieldedTransferTransition, + ShieldedTransferTransitionObjectJs, + ShieldedTransferTransitionJSONJs +); + +impl_wasm_type_info!(ShieldedTransferTransitionWasm, ShieldedTransferTransition); diff --git a/packages/wasm-dpp2/src/shielded/shielded_withdrawal_transition.rs b/packages/wasm-dpp2/src/shielded/shielded_withdrawal_transition.rs new file mode 100644 index 00000000000..cd0c85f47fb --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/shielded_withdrawal_transition.rs @@ -0,0 +1,193 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; +use dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * ShieldedWithdrawalTransition serialized as a plain object. + */ +export interface ShieldedWithdrawalTransitionObject { + $formatVersion: string; + actions: SerializedOrchardAction[]; + unshieldingAmount: bigint; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; + coreFeePerByte: number; + pooling: number; + outputScript: Uint8Array; +} + +/** + * ShieldedWithdrawalTransition serialized as JSON (human-readable). + */ +export interface ShieldedWithdrawalTransitionJSON { + $formatVersion: string; + actions: SerializedOrchardActionJSON[]; + unshieldingAmount: number | string; + anchor: string; + proof: string; + bindingSignature: string; + coreFeePerByte: number; + pooling: number; + outputScript: string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ShieldedWithdrawalTransitionObject")] + pub type ShieldedWithdrawalTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "ShieldedWithdrawalTransitionJSON")] + pub type ShieldedWithdrawalTransitionJSONJs; +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(transparent)] +#[wasm_bindgen(js_name = ShieldedWithdrawalTransition)] +pub struct ShieldedWithdrawalTransitionWasm(ShieldedWithdrawalTransition); + +impl From for ShieldedWithdrawalTransitionWasm { + fn from(v: ShieldedWithdrawalTransition) -> Self { + ShieldedWithdrawalTransitionWasm(v) + } +} + +impl From for ShieldedWithdrawalTransition { + fn from(v: ShieldedWithdrawalTransitionWasm) -> Self { + v.0 + } +} + +#[wasm_bindgen(js_class = ShieldedWithdrawalTransition)] +impl ShieldedWithdrawalTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn new( + value: ShieldedWithdrawalTransitionObjectJs, + ) -> WasmDppResult { + let inner: ShieldedWithdrawalTransition = serde_wasm_bindgen::from_value(value.into()) + .map_err(|e| WasmDppError::serialization(e.to_string()))?; + Ok(ShieldedWithdrawalTransitionWasm(inner)) + } + + #[wasm_bindgen(js_name = getType)] + pub fn get_type(&self) -> u8 { + self.0.state_transition_type() as u8 + } + + /// Returns the serialized Orchard actions as a JS array. + #[wasm_bindgen(js_name = getActions)] + pub fn get_actions(&self) -> WasmDppResult { + let inner = match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => &v0.actions, + }; + serde_wasm_bindgen::to_value(inner).map_err(|e| WasmDppError::serialization(e.to_string())) + } + + /// Returns the unshielding amount. + #[wasm_bindgen(js_name = getUnshieldingAmount)] + pub fn get_unshielding_amount(&self) -> u64 { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.unshielding_amount, + } + } + + /// Returns the anchor (32-byte Merkle root). + #[wasm_bindgen(js_name = getAnchor)] + pub fn get_anchor(&self) -> Vec { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.anchor.to_vec(), + } + } + + /// Returns the Halo2 proof bytes. + #[wasm_bindgen(js_name = getProof)] + pub fn get_proof(&self) -> Vec { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.proof.clone(), + } + } + + /// Returns the RedPallas binding signature (64 bytes). + #[wasm_bindgen(js_name = getBindingSignature)] + pub fn get_binding_signature(&self) -> Vec { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.binding_signature.to_vec(), + } + } + + /// Returns the core fee per byte. + #[wasm_bindgen(js_name = getCoreFeePerByte)] + pub fn get_core_fee_per_byte(&self) -> u32 { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.core_fee_per_byte, + } + } + + /// Returns the pooling strategy as a u8. + #[wasm_bindgen(js_name = getPooling)] + pub fn get_pooling(&self) -> u8 { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.pooling as u8, + } + } + + /// Returns the output script (core address). + #[wasm_bindgen(js_name = getOutputScript)] + pub fn get_output_script(&self) -> Vec { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.output_script.as_bytes().to_vec(), + } + } + + #[wasm_bindgen(js_name = getModifiedDataIds)] + pub fn modified_data_ids(&self) -> Vec { + self.0 + .modified_data_ids() + .into_iter() + .map(IdentifierWasm::from) + .collect() + } + + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> WasmDppResult> { + Ok(PlatformSerializable::serialize_to_bytes( + &StateTransition::ShieldedWithdrawal(self.0.clone()), + )?) + } + + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes(bytes: Vec) -> WasmDppResult { + let st = StateTransition::deserialize_from_bytes(&bytes)?; + match st { + StateTransition::ShieldedWithdrawal(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected ShieldedWithdrawal", + )), + } + } + + #[wasm_bindgen(js_name = toStateTransition)] + pub fn to_state_transition(&self) -> crate::state_transitions::base::StateTransitionWasm { + StateTransition::ShieldedWithdrawal(self.0.clone()).into() + } +} + +impl_wasm_conversions_serde!( + ShieldedWithdrawalTransitionWasm, + ShieldedWithdrawalTransition, + ShieldedWithdrawalTransitionObjectJs, + ShieldedWithdrawalTransitionJSONJs +); + +impl_wasm_type_info!( + ShieldedWithdrawalTransitionWasm, + ShieldedWithdrawalTransition +); diff --git a/packages/wasm-dpp2/src/shielded/unshield_transition.rs b/packages/wasm-dpp2/src/shielded/unshield_transition.rs new file mode 100644 index 00000000000..9a529fae00c --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/unshield_transition.rs @@ -0,0 +1,169 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; +use dpp::state_transition::unshield_transition::UnshieldTransition; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * UnshieldTransition serialized as a plain object. + */ +export interface UnshieldTransitionObject { + $formatVersion: string; + outputAddress: object; + actions: SerializedOrchardAction[]; + unshieldingAmount: bigint; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} + +/** + * UnshieldTransition serialized as JSON (human-readable). + */ +export interface UnshieldTransitionJSON { + $formatVersion: string; + outputAddress: object; + actions: SerializedOrchardActionJSON[]; + unshieldingAmount: number | string; + anchor: string; + proof: string; + bindingSignature: string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "UnshieldTransitionObject")] + pub type UnshieldTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "UnshieldTransitionJSON")] + pub type UnshieldTransitionJSONJs; +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(transparent)] +#[wasm_bindgen(js_name = UnshieldTransition)] +pub struct UnshieldTransitionWasm(UnshieldTransition); + +impl From for UnshieldTransitionWasm { + fn from(v: UnshieldTransition) -> Self { + UnshieldTransitionWasm(v) + } +} + +impl From for UnshieldTransition { + fn from(v: UnshieldTransitionWasm) -> Self { + v.0 + } +} + +#[wasm_bindgen(js_class = UnshieldTransition)] +impl UnshieldTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn new(value: UnshieldTransitionObjectJs) -> WasmDppResult { + let inner: UnshieldTransition = serde_wasm_bindgen::from_value(value.into()) + .map_err(|e| WasmDppError::serialization(e.to_string()))?; + Ok(UnshieldTransitionWasm(inner)) + } + + #[wasm_bindgen(js_name = getType)] + pub fn get_type(&self) -> u8 { + self.0.state_transition_type() as u8 + } + + /// Returns the output address as a JS value (serialized PlatformAddress). + #[wasm_bindgen(js_name = getOutputAddress)] + pub fn get_output_address(&self) -> WasmDppResult { + let addr = match &self.0 { + UnshieldTransition::V0(v0) => &v0.output_address, + }; + serde_wasm_bindgen::to_value(addr).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 { + let inner = match &self.0 { + UnshieldTransition::V0(v0) => &v0.actions, + }; + serde_wasm_bindgen::to_value(inner).map_err(|e| WasmDppError::serialization(e.to_string())) + } + + /// Returns the unshielding amount. + #[wasm_bindgen(js_name = getUnshieldingAmount)] + pub fn get_unshielding_amount(&self) -> u64 { + match &self.0 { + UnshieldTransition::V0(v0) => v0.unshielding_amount, + } + } + + /// Returns the anchor (32-byte Merkle root). + #[wasm_bindgen(js_name = getAnchor)] + pub fn get_anchor(&self) -> Vec { + match &self.0 { + UnshieldTransition::V0(v0) => v0.anchor.to_vec(), + } + } + + /// Returns the Halo2 proof bytes. + #[wasm_bindgen(js_name = getProof)] + pub fn get_proof(&self) -> Vec { + match &self.0 { + UnshieldTransition::V0(v0) => v0.proof.clone(), + } + } + + /// Returns the RedPallas binding signature (64 bytes). + #[wasm_bindgen(js_name = getBindingSignature)] + pub fn get_binding_signature(&self) -> Vec { + match &self.0 { + UnshieldTransition::V0(v0) => v0.binding_signature.to_vec(), + } + } + + #[wasm_bindgen(js_name = getModifiedDataIds)] + pub fn modified_data_ids(&self) -> Vec { + self.0 + .modified_data_ids() + .into_iter() + .map(IdentifierWasm::from) + .collect() + } + + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> WasmDppResult> { + Ok(PlatformSerializable::serialize_to_bytes( + &StateTransition::Unshield(self.0.clone()), + )?) + } + + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes(bytes: Vec) -> WasmDppResult { + let st = StateTransition::deserialize_from_bytes(&bytes)?; + match st { + StateTransition::Unshield(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected Unshield", + )), + } + } + + #[wasm_bindgen(js_name = toStateTransition)] + pub fn to_state_transition(&self) -> crate::state_transitions::base::StateTransitionWasm { + StateTransition::Unshield(self.0.clone()).into() + } +} + +impl_wasm_conversions_serde!( + UnshieldTransitionWasm, + UnshieldTransition, + UnshieldTransitionObjectJs, + UnshieldTransitionJSONJs +); + +impl_wasm_type_info!(UnshieldTransitionWasm, UnshieldTransition); diff --git a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs index 5c7625f979c..d2dbe0a927e 100644 --- a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs @@ -401,7 +401,7 @@ impl StateTransitionWasm { | ShieldedTransfer(_) | Unshield(_) | ShieldFromAssetLock(_) - | ShieldedWithdrawal(_) => todo!("shielded transitions not yet implemented"), + | ShieldedWithdrawal(_) => None, } } @@ -428,7 +428,7 @@ impl StateTransitionWasm { | ShieldedTransfer(_) | Unshield(_) | ShieldFromAssetLock(_) - | ShieldedWithdrawal(_) => todo!("shielded transitions not yet implemented"), + | ShieldedWithdrawal(_) => None, } } @@ -571,7 +571,9 @@ impl StateTransitionWasm { | Unshield(_) | ShieldFromAssetLock(_) | ShieldedWithdrawal(_) => { - todo!("shielded transitions not yet implemented") + return Err(WasmDppError::invalid_argument( + "Cannot set owner for shielded transition", + )); } }; @@ -646,7 +648,9 @@ impl StateTransitionWasm { | Unshield(_) | ShieldFromAssetLock(_) | ShieldedWithdrawal(_) => { - todo!("shielded transitions not yet implemented") + return Err(WasmDppError::invalid_argument( + "Cannot set identity contract nonce for shielded transition", + )); } }; @@ -741,7 +745,9 @@ impl StateTransitionWasm { | Unshield(_) | ShieldFromAssetLock(_) | ShieldedWithdrawal(_) => { - todo!("shielded transitions not yet implemented") + return Err(WasmDppError::invalid_argument( + "Cannot set identity nonce for shielded transition", + )); } }; diff --git a/packages/wasm-dpp2/src/state_transitions/mod.rs b/packages/wasm-dpp2/src/state_transitions/mod.rs index 4b9a878544b..acd9c8ede76 100644 --- a/packages/wasm-dpp2/src/state_transitions/mod.rs +++ b/packages/wasm-dpp2/src/state_transitions/mod.rs @@ -1,6 +1,7 @@ pub mod base; pub mod batch; pub mod proof_result; +pub mod proof_result_shielded; pub use base::{GroupStateTransitionInfoWasm, StateTransitionWasm}; pub use proof_result::{StateTransitionProofResultTypeJs, convert_proof_result}; diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result.rs b/packages/wasm-dpp2/src/state_transitions/proof_result.rs index 7faa3284dfb..923824cce36 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result.rs @@ -57,7 +57,11 @@ export type StateTransitionProofResultType = | VerifiedNextDistribution | VerifiedAddressInfos | VerifiedIdentityFullWithAddressInfos - | VerifiedIdentityWithAddressInfos; + | VerifiedIdentityWithAddressInfos + | VerifiedAssetLockConsumed + | VerifiedShieldedNullifiers + | VerifiedShieldedNullifiersWithAddressInfos + | VerifiedShieldedNullifiersWithWithdrawalDocument; "#; #[wasm_bindgen] @@ -70,7 +74,7 @@ extern "C" { // Helper: build a plain JS object from key-value pairs // ============================================================================ -fn js_obj(entries: &[(&str, JsValue)]) -> JsValue { +pub(crate) fn js_obj(entries: &[(&str, JsValue)]) -> JsValue { let obj = js_sys::Object::new(); for (key, val) in entries { js_sys::Reflect::set(&obj, &(*key).into(), val).unwrap(); @@ -724,6 +728,15 @@ fn build_address_infos_map( })) } +/// Helper to build `Map` from shielded nullifier results. +fn build_nullifier_map(nullifiers: Vec<(Vec, bool)>) -> Map { + Map::from_entries(nullifiers.into_iter().map(|(nullifier, is_spent)| { + let key: JsValue = hex::encode(&nullifier).into(); + let val: JsValue = is_spent.into(); + (key, val) + })) +} + fn action_status_to_string(status: dpp::group::group_action_status::GroupActionStatus) -> String { match status { dpp::group::group_action_status::GroupActionStatus::ActionActive => { @@ -735,28 +748,13 @@ fn action_status_to_string(status: dpp::group::group_action_status::GroupActionS } } -// --- VerifiedShieldedPoolState --- - -#[wasm_bindgen(js_name = "VerifiedShieldedPoolState")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedShieldedPoolStateWasm { - pool_balance: Option, -} - -impl_wasm_type_info!(VerifiedShieldedPoolStateWasm, VerifiedShieldedPoolState); -impl_wasm_conversions_serde!(VerifiedShieldedPoolStateWasm, VerifiedShieldedPoolState); +// Shielded types are in proof_result_shielded module +use super::proof_result_shielded::{ + VerifiedAssetLockConsumedWasm, VerifiedShieldedNullifiersWasm, + VerifiedShieldedNullifiersWithAddressInfosWasm, + VerifiedShieldedNullifiersWithWithdrawalDocumentWasm, +}; -#[wasm_bindgen(js_class = VerifiedShieldedPoolState)] -impl VerifiedShieldedPoolStateWasm { - #[wasm_bindgen(getter, js_name = "poolBalance")] - pub fn pool_balance(&self) -> JsValue { - match self.pool_balance { - Some(b) => BigInt::from(b).into(), - None => JsValue::undefined(), - } - } -} /// Convert a Rust `StateTransitionProofResult` into the corresponding typed /// WASM wrapper, ready to be returned to JavaScript. pub fn convert_proof_result( @@ -918,11 +916,51 @@ pub fn convert_proof_result( .into() } - StateTransitionProofResult::VerifiedAssetLockConsumed(_) - | StateTransitionProofResult::VerifiedShieldedNullifiers(_) - | StateTransitionProofResult::VerifiedShieldedNullifiersWithAddressInfos(_, _) - | StateTransitionProofResult::VerifiedShieldedNullifiersWithWithdrawalDocument(_, _) => { - todo!("shielded proof results not yet implemented in wasm") + StateTransitionProofResult::VerifiedAssetLockConsumed(info) => { + use dpp::asset_lock::StoredAssetLockInfo; + use dpp::asset_lock::reduced_asset_lock_value::AssetLockValueGettersV0; + let (status, initial, remaining) = match info { + StoredAssetLockInfo::FullyConsumed => ("FullyConsumed".to_string(), None, None), + StoredAssetLockInfo::PartiallyConsumed(val) => ( + "PartiallyConsumed".to_string(), + Some(val.initial_credit_value()), + Some(val.remaining_credit_value()), + ), + StoredAssetLockInfo::NotPresent => ("NotPresent".to_string(), None, None), + }; + VerifiedAssetLockConsumedWasm::new(status, initial, remaining).into() + } + + StateTransitionProofResult::VerifiedShieldedNullifiers(nullifiers) => { + VerifiedShieldedNullifiersWasm::from_map(build_nullifier_map(nullifiers)).into() + } + + StateTransitionProofResult::VerifiedShieldedNullifiersWithAddressInfos( + nullifiers, + infos, + ) => VerifiedShieldedNullifiersWithAddressInfosWasm::new( + build_nullifier_map(nullifiers), + build_address_infos_map(infos), + ) + .into(), + + StateTransitionProofResult::VerifiedShieldedNullifiersWithWithdrawalDocument( + nullifiers, + docs, + ) => { + let doc_map = Map::from_entries(docs.into_iter().map(|(id, maybe_doc)| { + let key: JsValue = IdentifierWasm::from(id).to_base58().into(); + let val: JsValue = match maybe_doc { + Some(doc) => doc_to_wasm(doc).into(), + None => JsValue::undefined(), + }; + (key, val) + })); + VerifiedShieldedNullifiersWithWithdrawalDocumentWasm::new( + build_nullifier_map(nullifiers), + doc_map, + ) + .into() } }; diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result_shielded.rs b/packages/wasm-dpp2/src/state_transitions/proof_result_shielded.rs new file mode 100644 index 00000000000..8235e2a341e --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result_shielded.rs @@ -0,0 +1,216 @@ +//! Shielded pool WASM wrappers for `StateTransitionProofResult` variants. +//! +//! These types were extracted from `proof_result` to keep shielded-specific +//! code in its own module. + +use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_type_info; +use js_sys::{BigInt, Map}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; + +use super::proof_result::js_obj; + +// --- VerifiedShieldedPoolState --- + +#[wasm_bindgen(js_name = "VerifiedShieldedPoolState")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedShieldedPoolStateWasm { + pool_balance: Option, +} + +impl_wasm_type_info!(VerifiedShieldedPoolStateWasm, VerifiedShieldedPoolState); +impl_wasm_conversions_serde!(VerifiedShieldedPoolStateWasm, VerifiedShieldedPoolState); + +#[wasm_bindgen(js_class = VerifiedShieldedPoolState)] +impl VerifiedShieldedPoolStateWasm { + #[wasm_bindgen(getter, js_name = "poolBalance")] + pub fn pool_balance(&self) -> JsValue { + match self.pool_balance { + Some(b) => BigInt::from(b).into(), + None => JsValue::undefined(), + } + } +} + +// --- VerifiedAssetLockConsumed --- + +#[wasm_bindgen(js_name = "VerifiedAssetLockConsumed")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedAssetLockConsumedWasm { + #[wasm_bindgen(getter_with_clone)] + pub status: String, + initial_credit_value: Option, + remaining_credit_value: Option, +} + +#[wasm_bindgen(js_class = VerifiedAssetLockConsumed)] +impl VerifiedAssetLockConsumedWasm { + #[wasm_bindgen(getter, js_name = "initialCreditValue")] + pub fn initial_credit_value(&self) -> JsValue { + match self.initial_credit_value { + Some(v) => BigInt::from(v).into(), + None => JsValue::undefined(), + } + } + + #[wasm_bindgen(getter, js_name = "remainingCreditValue")] + pub fn remaining_credit_value(&self) -> JsValue { + match self.remaining_credit_value { + Some(v) => BigInt::from(v).into(), + None => JsValue::undefined(), + } + } +} + +impl VerifiedAssetLockConsumedWasm { + pub fn new( + status: String, + initial_credit_value: Option, + remaining_credit_value: Option, + ) -> Self { + Self { + status, + initial_credit_value, + remaining_credit_value, + } + } +} + +impl_wasm_type_info!(VerifiedAssetLockConsumedWasm, VerifiedAssetLockConsumed); +impl_wasm_conversions_serde!(VerifiedAssetLockConsumedWasm, VerifiedAssetLockConsumed); + +// --- VerifiedShieldedNullifiers --- + +#[wasm_bindgen(js_name = "VerifiedShieldedNullifiers")] +#[derive(Clone)] +pub struct VerifiedShieldedNullifiersWasm { + nullifiers: Map, // Map +} + +#[wasm_bindgen(js_class = VerifiedShieldedNullifiers)] +impl VerifiedShieldedNullifiersWasm { + #[wasm_bindgen(getter)] + pub fn nullifiers(&self) -> Map { + self.nullifiers.clone() + } + + #[wasm_bindgen(js_name = toObject)] + pub fn to_object(&self) -> JsValue { + js_obj(&[("nullifiers", self.nullifiers.clone().into())]) + } + + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> JsValue { + self.to_object() + } +} + +impl VerifiedShieldedNullifiersWasm { + pub fn from_map(nullifiers: Map) -> Self { + Self { nullifiers } + } +} + +impl_wasm_type_info!(VerifiedShieldedNullifiersWasm, VerifiedShieldedNullifiers); + +// --- VerifiedShieldedNullifiersWithAddressInfos --- + +#[wasm_bindgen(js_name = "VerifiedShieldedNullifiersWithAddressInfos")] +#[derive(Clone)] +pub struct VerifiedShieldedNullifiersWithAddressInfosWasm { + nullifiers: Map, + address_infos: Map, +} + +#[wasm_bindgen(js_class = VerifiedShieldedNullifiersWithAddressInfos)] +impl VerifiedShieldedNullifiersWithAddressInfosWasm { + #[wasm_bindgen(getter)] + pub fn nullifiers(&self) -> Map { + self.nullifiers.clone() + } + + #[wasm_bindgen(getter = "addressInfos")] + pub fn address_infos(&self) -> Map { + self.address_infos.clone() + } + + #[wasm_bindgen(js_name = toObject)] + pub fn to_object(&self) -> JsValue { + js_obj(&[ + ("nullifiers", self.nullifiers.clone().into()), + ("addressInfos", self.address_infos.clone().into()), + ]) + } + + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> JsValue { + self.to_object() + } +} + +impl VerifiedShieldedNullifiersWithAddressInfosWasm { + pub fn new(nullifiers: Map, address_infos: Map) -> Self { + Self { + nullifiers, + address_infos, + } + } +} + +impl_wasm_type_info!( + VerifiedShieldedNullifiersWithAddressInfosWasm, + VerifiedShieldedNullifiersWithAddressInfos +); + +// --- VerifiedShieldedNullifiersWithWithdrawalDocument --- + +#[wasm_bindgen(js_name = "VerifiedShieldedNullifiersWithWithdrawalDocument")] +#[derive(Clone)] +pub struct VerifiedShieldedNullifiersWithWithdrawalDocumentWasm { + nullifiers: Map, + documents: Map, +} + +#[wasm_bindgen(js_class = VerifiedShieldedNullifiersWithWithdrawalDocument)] +impl VerifiedShieldedNullifiersWithWithdrawalDocumentWasm { + #[wasm_bindgen(getter)] + pub fn nullifiers(&self) -> Map { + self.nullifiers.clone() + } + + #[wasm_bindgen(getter)] + pub fn documents(&self) -> Map { + self.documents.clone() + } + + #[wasm_bindgen(js_name = toObject)] + pub fn to_object(&self) -> JsValue { + js_obj(&[ + ("nullifiers", self.nullifiers.clone().into()), + ("documents", self.documents.clone().into()), + ]) + } + + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> JsValue { + self.to_object() + } +} + +impl VerifiedShieldedNullifiersWithWithdrawalDocumentWasm { + pub fn new(nullifiers: Map, documents: Map) -> Self { + Self { + nullifiers, + documents, + } + } +} + +impl_wasm_type_info!( + VerifiedShieldedNullifiersWithWithdrawalDocumentWasm, + VerifiedShieldedNullifiersWithWithdrawalDocument +); diff --git a/packages/wasm-sdk/src/lib.rs b/packages/wasm-sdk/src/lib.rs index 14a662bb7c3..592f7e01523 100644 --- a/packages/wasm-sdk/src/lib.rs +++ b/packages/wasm-sdk/src/lib.rs @@ -15,6 +15,7 @@ pub mod wallet; pub use dpns::*; pub use error::{WasmSdkError, WasmSdkErrorKind}; pub use queries::{ + shielded::{ShieldedEncryptedNoteWasm, ShieldedNullifierStatusWasm}, PlatformAddressInfoWasm, ProofInfoWasm, ProofMetadataResponseWasm, ResponseMetadataWasm, }; pub use state_transitions::identity as state_transition_identity; diff --git a/packages/wasm-sdk/src/queries/mod.rs b/packages/wasm-sdk/src/queries/mod.rs index 14e9eef5dc9..8554dd2dac5 100644 --- a/packages/wasm-sdk/src/queries/mod.rs +++ b/packages/wasm-sdk/src/queries/mod.rs @@ -5,6 +5,7 @@ pub mod epoch; pub mod group; pub mod identity; pub mod protocol; +pub mod shielded; pub mod system; pub mod token; pub(crate) mod utils; diff --git a/packages/wasm-sdk/src/queries/shielded.rs b/packages/wasm-sdk/src/queries/shielded.rs new file mode 100644 index 00000000000..7843513d14d --- /dev/null +++ b/packages/wasm-sdk/src/queries/shielded.rs @@ -0,0 +1,362 @@ +use crate::error::WasmSdkError; +use crate::impl_wasm_serde_conversions; +use crate::queries::ProofMetadataResponseWasm; +use crate::sdk::WasmSdk; +use js_sys::{Array, BigInt, Uint8Array}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; +use wasm_dpp2::serialization::bytes_b64; + +// ── TypeScript interface declarations ────────────────────────────────── + +#[wasm_bindgen(typescript_custom_section)] +const TS_SHIELDED: &'static str = r#" +export interface IShieldedEncryptedNote { + readonly cmx: Uint8Array; + readonly nullifier: Uint8Array; + readonly encryptedNote: Uint8Array; +} + +export interface IShieldedNullifierStatus { + readonly nullifier: Uint8Array; + readonly isSpent: boolean; +} +"#; + +// ── Wrapper types ────────────────────────────────────────────────────── + +#[wasm_bindgen(js_name = "ShieldedEncryptedNote")] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ShieldedEncryptedNoteWasm { + #[serde(with = "bytes_b64")] + cmx: Vec, + #[serde(with = "bytes_b64")] + nullifier: Vec, + #[serde(with = "bytes_b64")] + encrypted_note: Vec, +} + +#[wasm_bindgen(js_class = ShieldedEncryptedNote)] +impl ShieldedEncryptedNoteWasm { + #[wasm_bindgen(getter)] + pub fn cmx(&self) -> Uint8Array { + Uint8Array::from(self.cmx.as_slice()) + } + + #[wasm_bindgen(getter)] + pub fn nullifier(&self) -> Uint8Array { + Uint8Array::from(self.nullifier.as_slice()) + } + + #[wasm_bindgen(getter = encryptedNote)] + pub fn encrypted_note(&self) -> Uint8Array { + Uint8Array::from(self.encrypted_note.as_slice()) + } +} +impl_wasm_serde_conversions!(ShieldedEncryptedNoteWasm, ShieldedEncryptedNote); + +#[wasm_bindgen(js_name = "ShieldedNullifierStatus")] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ShieldedNullifierStatusWasm { + #[serde(with = "bytes_b64")] + nullifier: Vec, + is_spent: bool, +} + +#[wasm_bindgen(js_class = ShieldedNullifierStatus)] +impl ShieldedNullifierStatusWasm { + #[wasm_bindgen(getter)] + pub fn nullifier(&self) -> Uint8Array { + Uint8Array::from(self.nullifier.as_slice()) + } + + #[wasm_bindgen(getter = isSpent)] + pub fn is_spent(&self) -> bool { + self.is_spent + } +} +impl_wasm_serde_conversions!(ShieldedNullifierStatusWasm, ShieldedNullifierStatus); + +// ── Helpers ─────────────────────────────────────────────────────────── + +/// Parse a JS `Array` of nullifiers into `Vec<[u8; 32]>`. +/// +/// Each element must be exactly 32 bytes, otherwise an error is returned. +fn parse_nullifiers(nullifiers: &js_sys::Array) -> Result, WasmSdkError> { + nullifiers + .iter() + .map(|n| { + let uint8_arr = Uint8Array::new(&n); + let bytes = uint8_arr.to_vec(); + if bytes.len() != 32 { + return Err(WasmSdkError::invalid_argument( + "Each nullifier must be exactly 32 bytes", + )); + } + let mut arr = [0u8; 32]; + arr.copy_from_slice(&bytes); + Ok(arr) + }) + .collect() +} + +// ── Query methods ────────────────────────────────────────────────────── + +#[wasm_bindgen] +impl WasmSdk { + /// Returns the total shielded pool balance as a BigInt, or undefined if not available. + #[wasm_bindgen( + js_name = "getShieldedPoolState", + unchecked_return_type = "bigint | undefined" + )] + pub async fn get_shielded_pool_state(&self) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{NoParamQuery, ShieldedPoolState}; + + let result = ShieldedPoolState::fetch(self.as_ref(), NoParamQuery {}).await?; + match result { + Some(s) => Ok(JsValue::from(BigInt::from(s.0))), + None => Ok(JsValue::UNDEFINED), + } + } + + /// Fetches encrypted notes from the shielded pool, paginated. + #[wasm_bindgen( + js_name = "getShieldedEncryptedNotes", + unchecked_return_type = "Array" + )] + pub async fn get_shielded_encrypted_notes( + &self, + #[wasm_bindgen(js_name = "startIndex")] start_index: u64, + count: u32, + ) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{ShieldedEncryptedNotes, ShieldedEncryptedNotesQuery}; + + let query = ShieldedEncryptedNotesQuery { start_index, count }; + let result = ShieldedEncryptedNotes::fetch(self.as_ref(), query).await?; + + let array = Array::new(); + if let Some(notes) = result { + for note in notes.0 { + array.push(&JsValue::from(ShieldedEncryptedNoteWasm { + cmx: note.cmx, + nullifier: note.nullifier, + encrypted_note: note.encrypted_note, + })); + } + } + Ok(array) + } + + /// Returns all valid anchors for building Orchard spend proofs. + #[wasm_bindgen( + js_name = "getShieldedAnchors", + unchecked_return_type = "Array" + )] + pub async fn get_shielded_anchors(&self) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{NoParamQuery, ShieldedAnchors}; + + let result = ShieldedAnchors::fetch(self.as_ref(), NoParamQuery {}).await?; + + let array = Array::new(); + if let Some(anchors) = result { + for anchor in anchors.0 { + array.push(&Uint8Array::from(anchor.as_slice()).into()); + } + } + Ok(array) + } + + /// Returns the most recent shielded anchor (32 bytes), or undefined if none exists. + #[wasm_bindgen( + js_name = "getMostRecentShieldedAnchor", + unchecked_return_type = "Uint8Array | undefined" + )] + pub async fn get_most_recent_shielded_anchor(&self) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{MostRecentShieldedAnchor, NoParamQuery}; + + let result = MostRecentShieldedAnchor::fetch(self.as_ref(), NoParamQuery {}).await?; + match result { + Some(anchor) => Ok(Uint8Array::from(anchor.0.as_slice()).into()), + None => Ok(JsValue::UNDEFINED), + } + } + + /// Checks the spent/unspent status of one or more nullifiers. + #[wasm_bindgen( + js_name = "getShieldedNullifiers", + unchecked_return_type = "Array" + )] + pub async fn get_shielded_nullifiers( + &self, + nullifiers: js_sys::Array, + ) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{ShieldedNullifierStatuses, ShieldedNullifiersQuery}; + + let nullifier_arrays = parse_nullifiers(&nullifiers)?; + + let query = ShieldedNullifiersQuery(nullifier_arrays); + let result = ShieldedNullifierStatuses::fetch(self.as_ref(), query).await?; + + let array = Array::new(); + if let Some(statuses) = result { + for status in statuses.0 { + array.push(&JsValue::from(ShieldedNullifierStatusWasm { + nullifier: status.nullifier.to_vec(), + is_spent: status.is_spent, + })); + } + } + Ok(array) + } + + // ── Proof variants ───────────────────────────────────────────────── + + #[wasm_bindgen( + js_name = "getShieldedPoolStateWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped" + )] + pub async fn get_shielded_pool_state_with_proof_info( + &self, + ) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{NoParamQuery, ShieldedPoolState}; + + let (result, metadata, proof) = + ShieldedPoolState::fetch_with_metadata_and_proof(self.as_ref(), NoParamQuery {}, None) + .await?; + + let data = result + .map(|s| JsValue::from(BigInt::from(s.0))) + .unwrap_or(JsValue::UNDEFINED); + + Ok(ProofMetadataResponseWasm::from_sdk_parts( + data, metadata, proof, + )) + } + + #[wasm_bindgen( + js_name = "getShieldedEncryptedNotesWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped>" + )] + pub async fn get_shielded_encrypted_notes_with_proof_info( + &self, + #[wasm_bindgen(js_name = "startIndex")] start_index: u64, + count: u32, + ) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{ShieldedEncryptedNotes, ShieldedEncryptedNotesQuery}; + + let query = ShieldedEncryptedNotesQuery { start_index, count }; + let (result, metadata, proof) = + ShieldedEncryptedNotes::fetch_with_metadata_and_proof(self.as_ref(), query, None) + .await?; + + let array = Array::new(); + if let Some(notes) = result { + for note in notes.0 { + array.push(&JsValue::from(ShieldedEncryptedNoteWasm { + cmx: note.cmx, + nullifier: note.nullifier, + encrypted_note: note.encrypted_note, + })); + } + } + + Ok(ProofMetadataResponseWasm::from_sdk_parts( + array, metadata, proof, + )) + } + + #[wasm_bindgen( + js_name = "getShieldedAnchorsWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped>" + )] + pub async fn get_shielded_anchors_with_proof_info( + &self, + ) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{NoParamQuery, ShieldedAnchors}; + + let (result, metadata, proof) = + ShieldedAnchors::fetch_with_metadata_and_proof(self.as_ref(), NoParamQuery {}, None) + .await?; + + let array = Array::new(); + if let Some(anchors) = result { + for anchor in anchors.0 { + array.push(&Uint8Array::from(anchor.as_slice()).into()); + } + } + + Ok(ProofMetadataResponseWasm::from_sdk_parts( + array, metadata, proof, + )) + } + + #[wasm_bindgen( + js_name = "getMostRecentShieldedAnchorWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped" + )] + pub async fn get_most_recent_shielded_anchor_with_proof_info( + &self, + ) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{MostRecentShieldedAnchor, NoParamQuery}; + + let (result, metadata, proof) = MostRecentShieldedAnchor::fetch_with_metadata_and_proof( + self.as_ref(), + NoParamQuery {}, + None, + ) + .await?; + + let data = result + .map(|a| JsValue::from(Uint8Array::from(a.0.as_slice()))) + .unwrap_or(JsValue::UNDEFINED); + + Ok(ProofMetadataResponseWasm::from_sdk_parts( + data, metadata, proof, + )) + } + + #[wasm_bindgen( + js_name = "getShieldedNullifiersWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped>" + )] + pub async fn get_shielded_nullifiers_with_proof_info( + &self, + nullifiers: js_sys::Array, + ) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{ShieldedNullifierStatuses, ShieldedNullifiersQuery}; + + let nullifier_arrays = parse_nullifiers(&nullifiers)?; + + let query = ShieldedNullifiersQuery(nullifier_arrays); + let (result, metadata, proof) = + ShieldedNullifierStatuses::fetch_with_metadata_and_proof(self.as_ref(), query, None) + .await?; + + let array = Array::new(); + if let Some(statuses) = result { + for status in statuses.0 { + array.push(&JsValue::from(ShieldedNullifierStatusWasm { + nullifier: status.nullifier.to_vec(), + is_spent: status.is_spent, + })); + } + } + + Ok(ProofMetadataResponseWasm::from_sdk_parts( + array, metadata, proof, + )) + } +}