diff --git a/.gitignore b/.gitignore index 3b9d6413736..89ce72920f3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ # ignore VSCode project specific files .vscode +# ignore Serena MCP project-local cache/config +.serena + # Env file .env diff --git a/packages/js-evo-sdk/src/sdk.ts b/packages/js-evo-sdk/src/sdk.ts index 81bcab56d9a..8bfeb46574c 100644 --- a/packages/js-evo-sdk/src/sdk.ts +++ b/packages/js-evo-sdk/src/sdk.ts @@ -12,6 +12,7 @@ import { StateTransitionsFacade } from './state-transitions/facade.js'; import { SystemFacade } from './system/facade.js'; import { GroupFacade } from './group/facade.js'; import { VotingFacade } from './voting/facade.js'; +import { ShieldedFacade } from './shielded/facade.js'; export interface ConnectionOptions { version?: number; @@ -52,6 +53,7 @@ export class EvoSDK { public system!: SystemFacade; public group!: GroupFacade; public voting!: VotingFacade; + public shielded!: ShieldedFacade; constructor(options: EvoSDKOptions = {}) { // Apply defaults while preserving any future connection options const { network = 'testnet', trusted = false, addresses, ...connection } = options; @@ -69,6 +71,7 @@ export class EvoSDK { this.system = new SystemFacade(this); this.group = new GroupFacade(this); this.voting = new VotingFacade(this); + this.shielded = new ShieldedFacade(this); } get wasm(): wasm.WasmSdk { @@ -209,5 +212,6 @@ export { StateTransitionsFacade } from './state-transitions/facade.js'; export { SystemFacade } from './system/facade.js'; export { GroupFacade } from './group/facade.js'; export { VotingFacade } from './voting/facade.js'; +export { ShieldedFacade } from './shielded/facade.js'; export { wallet } from './wallet/functions.js'; export * from './wasm.js'; diff --git a/packages/js-evo-sdk/src/shielded/facade.ts b/packages/js-evo-sdk/src/shielded/facade.ts new file mode 100644 index 00000000000..7eee99d1559 --- /dev/null +++ b/packages/js-evo-sdk/src/shielded/facade.ts @@ -0,0 +1,86 @@ +import * as wasm from '../wasm.js'; +import type { EvoSDK } from '../sdk.js'; + +/** + * Read-only access to the shielded (Orchard) pool: total balance, encrypted + * notes, anchors, nullifier statuses. Each query has a `*WithProof` variant + * that additionally returns proof + metadata for cryptographic verification. + * + * Building / signing / broadcasting shielded transitions is intentionally + * out of scope here — that requires the Orchard prover and is tracked + * separately. See PR #3235 for the deferral rationale. + */ +export class ShieldedFacade { + private sdk: EvoSDK; + + constructor(sdk: EvoSDK) { + this.sdk = sdk; + } + + // ── Pool state ──────────────────────────────────────────────────────── + + async poolState(): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.getShieldedPoolState(); + } + + async poolStateWithProof(): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.getShieldedPoolStateWithProofInfo(); + } + + // ── Encrypted notes ────────────────────────────────────────────────── + + async encryptedNotes(startIndex: bigint, count: number): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.getShieldedEncryptedNotes(startIndex, count); + } + + async encryptedNotesWithProof( + startIndex: bigint, + count: number, + ): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.getShieldedEncryptedNotesWithProofInfo(startIndex, count); + } + + // ── Anchors ────────────────────────────────────────────────────────── + + async anchors(): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.getShieldedAnchors(); + } + + async anchorsWithProof(): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.getShieldedAnchorsWithProofInfo(); + } + + async mostRecentAnchor(): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.getMostRecentShieldedAnchor(); + } + + async mostRecentAnchorWithProof(): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.getMostRecentShieldedAnchorWithProofInfo(); + } + + // ── Nullifiers ─────────────────────────────────────────────────────── + + /** + * Each nullifier must be a `Uint8Array` of exactly 32 bytes; otherwise + * the underlying call rejects with `InvalidArgument`. + */ + async nullifiers(nullifiers: Uint8Array[]): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.getShieldedNullifiers(nullifiers); + } + + async nullifiersWithProof( + nullifiers: Uint8Array[], + ): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.getShieldedNullifiersWithProofInfo(nullifiers); + } +} diff --git a/packages/js-evo-sdk/tests/unit/facades/shielded.spec.ts b/packages/js-evo-sdk/tests/unit/facades/shielded.spec.ts new file mode 100644 index 00000000000..97fd2a28708 --- /dev/null +++ b/packages/js-evo-sdk/tests/unit/facades/shielded.spec.ts @@ -0,0 +1,121 @@ +import type { SinonStub } from 'sinon'; +import init, * as wasmSDKPackage from '@dashevo/wasm-sdk'; +import { EvoSDK } from '../../../dist/sdk.js'; + +describe('ShieldedFacade', () => { + let wasmSdk: wasmSDKPackage.WasmSdk; + let client: EvoSDK; + + let getShieldedPoolStateStub: SinonStub; + let getShieldedPoolStateWithProofInfoStub: SinonStub; + let getShieldedEncryptedNotesStub: SinonStub; + let getShieldedEncryptedNotesWithProofInfoStub: SinonStub; + let getShieldedAnchorsStub: SinonStub; + let getShieldedAnchorsWithProofInfoStub: SinonStub; + let getMostRecentShieldedAnchorStub: SinonStub; + let getMostRecentShieldedAnchorWithProofInfoStub: SinonStub; + let getShieldedNullifiersStub: SinonStub; + let getShieldedNullifiersWithProofInfoStub: SinonStub; + + beforeEach(async function setup() { + await init(); + const builder = wasmSDKPackage.WasmSdkBuilder.testnet(); + wasmSdk = await builder.build(); + client = EvoSDK.fromWasm(wasmSdk); + + getShieldedPoolStateStub = this.sinon.stub(wasmSdk, 'getShieldedPoolState').resolves(1000n); + getShieldedPoolStateWithProofInfoStub = this.sinon.stub(wasmSdk, 'getShieldedPoolStateWithProofInfo').resolves('proof'); + getShieldedEncryptedNotesStub = this.sinon.stub(wasmSdk, 'getShieldedEncryptedNotes').resolves([]); + getShieldedEncryptedNotesWithProofInfoStub = this.sinon.stub(wasmSdk, 'getShieldedEncryptedNotesWithProofInfo').resolves('proof'); + getShieldedAnchorsStub = this.sinon.stub(wasmSdk, 'getShieldedAnchors').resolves([]); + getShieldedAnchorsWithProofInfoStub = this.sinon.stub(wasmSdk, 'getShieldedAnchorsWithProofInfo').resolves('proof'); + getMostRecentShieldedAnchorStub = this.sinon.stub(wasmSdk, 'getMostRecentShieldedAnchor').resolves(undefined); + getMostRecentShieldedAnchorWithProofInfoStub = this.sinon.stub(wasmSdk, 'getMostRecentShieldedAnchorWithProofInfo').resolves('proof'); + getShieldedNullifiersStub = this.sinon.stub(wasmSdk, 'getShieldedNullifiers').resolves([]); + getShieldedNullifiersWithProofInfoStub = this.sinon.stub(wasmSdk, 'getShieldedNullifiersWithProofInfo').resolves('proof'); + }); + + describe('poolState()', () => { + it('should forward to getShieldedPoolState and return its result', async () => { + const result = await client.shielded.poolState(); + expect(getShieldedPoolStateStub).to.be.calledOnce(); + expect(result).to.equal(1000n); + }); + }); + + describe('poolStateWithProof()', () => { + it('should forward to getShieldedPoolStateWithProofInfo', async () => { + await client.shielded.poolStateWithProof(); + expect(getShieldedPoolStateWithProofInfoStub).to.be.calledOnce(); + }); + }); + + describe('encryptedNotes()', () => { + it('should forward startIndex/count to getShieldedEncryptedNotes', async () => { + await client.shielded.encryptedNotes(0n, 10); + expect(getShieldedEncryptedNotesStub).to.be.calledOnceWithExactly(0n, 10); + }); + + it('should pass through bigint startIndex without truncation', async () => { + const big = 9_007_199_254_740_993n; // > Number.MAX_SAFE_INTEGER + await client.shielded.encryptedNotes(big, 5); + expect(getShieldedEncryptedNotesStub).to.be.calledWith(big, 5); + }); + }); + + describe('encryptedNotesWithProof()', () => { + it('should forward startIndex/count to getShieldedEncryptedNotesWithProofInfo', async () => { + await client.shielded.encryptedNotesWithProof(100n, 25); + expect(getShieldedEncryptedNotesWithProofInfoStub).to.be.calledOnceWithExactly(100n, 25); + }); + }); + + describe('anchors()', () => { + it('should forward to getShieldedAnchors', async () => { + await client.shielded.anchors(); + expect(getShieldedAnchorsStub).to.be.calledOnce(); + }); + }); + + describe('anchorsWithProof()', () => { + it('should forward to getShieldedAnchorsWithProofInfo', async () => { + await client.shielded.anchorsWithProof(); + expect(getShieldedAnchorsWithProofInfoStub).to.be.calledOnce(); + }); + }); + + describe('mostRecentAnchor()', () => { + it('should forward to getMostRecentShieldedAnchor', async () => { + await client.shielded.mostRecentAnchor(); + expect(getMostRecentShieldedAnchorStub).to.be.calledOnce(); + }); + }); + + describe('mostRecentAnchorWithProof()', () => { + it('should forward to getMostRecentShieldedAnchorWithProofInfo', async () => { + await client.shielded.mostRecentAnchorWithProof(); + expect(getMostRecentShieldedAnchorWithProofInfoStub).to.be.calledOnce(); + }); + }); + + describe('nullifiers()', () => { + it('should forward the nullifier array to getShieldedNullifiers', async () => { + const nullifiers = [new Uint8Array(32).fill(1), new Uint8Array(32).fill(2)]; + await client.shielded.nullifiers(nullifiers); + expect(getShieldedNullifiersStub).to.be.calledOnceWithExactly(nullifiers); + }); + + it('should accept an empty array', async () => { + await client.shielded.nullifiers([]); + expect(getShieldedNullifiersStub).to.be.calledOnceWithExactly([]); + }); + }); + + describe('nullifiersWithProof()', () => { + it('should forward the nullifier array to getShieldedNullifiersWithProofInfo', async () => { + const nullifiers = [new Uint8Array(32).fill(7)]; + await client.shielded.nullifiersWithProof(nullifiers); + expect(getShieldedNullifiersWithProofInfoStub).to.be.calledOnceWithExactly(nullifiers); + }); + }); +}); diff --git a/packages/rs-dpp-json-convertible-derive/src/lib.rs b/packages/rs-dpp-json-convertible-derive/src/lib.rs index c6b1fe04240..83d81044ac4 100644 --- a/packages/rs-dpp-json-convertible-derive/src/lib.rs +++ b/packages/rs-dpp-json-convertible-derive/src/lib.rs @@ -7,25 +7,50 @@ use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Item, Type}; const DEFAULT_BASE_PATH: &str = "crate::serialization"; -/// Attribute macro that adds `#[serde(with = "...")]` to `u64`/`i64` fields -/// and implements `JsonSafeFields` for the type (when used inside the `dpp` crate). +/// Attribute macro that auto-injects `#[serde(with = "...")]` on fields whose +/// natural serde shape would round-trip badly through JSON / WASM. /// /// Works on both **structs** and **enums** (with named fields in variants). /// -/// Matches literal `u64`, `i64`, `Option`, `Option`, and known type aliases -/// (e.g. `Credits`, `TokenAmount`, `TimestampMillis`, `BlockHeight`). -/// The macro skips fields that already have `#[serde(with)]`. +/// # What it injects +/// +/// | Field type | Helper injected | Wire shape | +/// |---|---|---| +/// | `u64` / `Option` and known aliases (`Credits`, `TokenAmount`, `TimestampMillis`, `BlockHeight`, …) | `json_safe_u64` / `json_safe_option_u64` | number ≤ `MAX_SAFE_INTEGER`, otherwise string — avoids JS precision loss | +/// | `i64` / `Option` and known aliases | `json_safe_i64` / `json_safe_option_i64` | same, signed | +/// | `[u8; N]` (any `N`, via const generics) | `serde_bytes` | raw bytes in binary, base64 string in JSON | +/// | `Vec` | `serde_bytes_var` | raw bytes in binary, base64 string in JSON | +/// +/// The byte-field handling matches the broader codebase convention used by +/// `Bytes20` / `Bytes32` / `Bytes36` / `BinaryData` in `rs-platform-value`. +/// +/// Fields that already carry an explicit `#[serde(with = "…")]` (or +/// `skip` / `flatten` / `cfg_attr` wrapping a `serde(with)`) are left +/// untouched. +/// +/// # Compile-time transitive guarantee +/// +/// When applied inside the `dpp` crate, the macro also implements the marker +/// trait `JsonSafeFields` for the type, and emits compile-time assertions that +/// every nested non-primitive field type (anything not directly handled above) +/// also implements `JsonSafeFields`. This propagates the safety check +/// transitively — adding a new struct that contains a `Credits` without an +/// `#[json_safe_fields]` annotation will fail to compile. +/// +/// # Feature flags /// /// When serde derives are behind `cfg_attr(feature = "...")`, the `cfg_attr` is -/// evaluated by the compiler BEFORE this macro runs. If the feature is off, serde -/// derives aren't visible and `#[serde(with)]` is NOT generated — which is correct -/// because `serde(with)` requires an active serde derive. +/// evaluated by the compiler BEFORE this macro runs. If the feature is off, +/// serde derives aren't visible and `#[serde(with)]` is NOT generated — which +/// is correct because `serde(with)` requires an active serde derive. /// /// # Inside `dpp` crate (default) /// ```ignore /// #[json_safe_fields] /// pub struct MyStructV0 { /// pub supply: u64, // → auto-annotated with crate::serialization::json_safe_u64 +/// pub anchor: [u8; 32], // → auto-annotated with crate::serialization::serde_bytes +/// pub proof: Vec, // → auto-annotated with crate::serialization::serde_bytes_var /// pub name: String, // → untouched /// } /// ``` @@ -290,12 +315,18 @@ fn annotate_fields(fields: &mut syn::FieldsNamed, base_path: &str) -> Vec /// Determine if a type needs a serde `with` annotation and return the module name suffix. /// -/// Matches literal `u64`, `i64`, `Option`, `Option`, and known type aliases -/// that resolve to u64/i64 (e.g. `Credits`, `TokenAmount`, `TimestampMillis`). +/// Matches: +/// - `u64`, `i64`, `Option`, `Option`, and known u64/i64 type aliases +/// (e.g. `Credits`, `TokenAmount`, `TimestampMillis`) → `json_safe_*` helpers +/// - `[u8; N]` for any `N` → `serde_bytes` (const-generic; raw bytes in binary, +/// base64 string in JSON) +/// - `Vec` → `serde_bytes_var` (variable-length variant of the above) /// /// When adding a new `type X = u64` alias in rs-dpp, add it to the appropriate list below. fn serde_with_suffix_for_type(ty: &Type) -> Option<&'static str> { match ty { + // [u8; N] — any fixed-size byte array, length-agnostic via const generics. + Type::Array(arr) if is_u8_type(&arr.elem) => Some("serde_bytes"), Type::Path(type_path) => { let segments = &type_path.path.segments; // Get the last segment — handles both `u64` and `std::u64` / `crate::prelude::Credits` @@ -308,6 +339,18 @@ fn serde_with_suffix_for_type(ty: &Type) -> Option<&'static str> { if is_i64_type(ident) { return Some("json_safe_i64"); } + // Vec → variable-length bytes helper (base64 in JSON, raw in binary). + if ident == "Vec" { + if let syn::PathArguments::AngleBracketed(args) = &last.arguments { + if args.args.len() == 1 { + if let syn::GenericArgument::Type(inner_ty) = &args.args[0] { + if is_u8_type(inner_ty) { + return Some("serde_bytes_var"); + } + } + } + } + } // Check for Option — handles both `Option` and `std::option::Option` if ident == "Option" { if let syn::PathArguments::AngleBracketed(args) = &last.arguments { @@ -431,6 +474,17 @@ fn is_i64_type(ident: &Ident) -> bool { ident == "i64" || I64_ALIASES.iter().any(|alias| ident == alias) } +/// True when the type resolves to `u8` — used to detect `[u8; N]` and `Vec` +/// byte fields and auto-inject the matching `serde_bytes_*` helper. +fn is_u8_type(ty: &Type) -> bool { + if let Type::Path(type_path) = ty { + if let Some(last) = type_path.path.segments.last() { + return last.ident == "u8" && last.arguments.is_empty(); + } + } + false +} + /// Derive macro that generates `impl JsonConvertible for Type {}` with /// compile-time assertions that all inner types implement `JsonSafeFields`. /// diff --git a/packages/rs-dpp/src/address_funds/fee_strategy/mod.rs b/packages/rs-dpp/src/address_funds/fee_strategy/mod.rs index 98490da190d..f34728902d2 100644 --- a/packages/rs-dpp/src/address_funds/fee_strategy/mod.rs +++ b/packages/rs-dpp/src/address_funds/fee_strategy/mod.rs @@ -7,11 +7,6 @@ use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Encode, Decode, PartialEq, Eq, Hash)] -#[cfg_attr( - feature = "serde-conversion", - derive(Serialize, Deserialize), - serde(rename_all = "camelCase") -)] pub enum AddressFundsFeeStrategyStep { /// Deduct fee from a specific input address by index. /// The input must have remaining balance after its contribution to outputs. @@ -28,3 +23,156 @@ impl Default for AddressFundsFeeStrategyStep { } pub type AddressFundsFeeStrategy = Vec; + +// Custom serde impls so JSON / wasm Object output uses the standard +// `{ "type": "...", "index": N }` discriminator shape used elsewhere in +// the DPP wasm bindings. The bincode `Encode` / `Decode` derives above are +// the consensus-critical binary format and are intentionally untouched. +#[cfg(feature = "serde-conversion")] +impl Serialize for AddressFundsFeeStrategyStep { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + let mut state = serializer.serialize_struct("AddressFundsFeeStrategyStep", 2)?; + match self { + AddressFundsFeeStrategyStep::DeductFromInput(index) => { + state.serialize_field("type", "deductFromInput")?; + state.serialize_field("index", index)?; + } + AddressFundsFeeStrategyStep::ReduceOutput(index) => { + state.serialize_field("type", "reduceOutput")?; + state.serialize_field("index", index)?; + } + } + state.end() + } +} + +#[cfg(feature = "serde-conversion")] +impl<'de> Deserialize<'de> for AddressFundsFeeStrategyStep { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{self, MapAccess, Visitor}; + use std::fmt; + + struct StepVisitor; + + impl<'de> Visitor<'de> for StepVisitor { + type Value = AddressFundsFeeStrategyStep; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an AddressFundsFeeStrategyStep struct with type and index") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut step_type: Option = None; + let mut index: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "type" => { + if step_type.is_some() { + return Err(de::Error::duplicate_field("type")); + } + step_type = Some(map.next_value()?); + } + "index" => { + if index.is_some() { + return Err(de::Error::duplicate_field("index")); + } + index = Some(map.next_value()?); + } + _ => { + let _: serde::de::IgnoredAny = map.next_value()?; + } + } + } + + let step_type = step_type.ok_or_else(|| de::Error::missing_field("type"))?; + let index = index.ok_or_else(|| de::Error::missing_field("index"))?; + + match step_type.as_str() { + "deductFromInput" => Ok(AddressFundsFeeStrategyStep::DeductFromInput(index)), + "reduceOutput" => Ok(AddressFundsFeeStrategyStep::ReduceOutput(index)), + other => Err(de::Error::unknown_variant( + other, + &["deductFromInput", "reduceOutput"], + )), + } + } + } + + deserializer.deserialize_struct( + "AddressFundsFeeStrategyStep", + &["type", "index"], + StepVisitor, + ) + } +} + +#[cfg(all(test, feature = "serde-conversion"))] +mod tests { + use super::*; + + #[test] + fn deduct_from_input_serializes_with_type_and_index() { + let step = AddressFundsFeeStrategyStep::DeductFromInput(7); + let json = serde_json::to_value(&step).unwrap(); + assert_eq!( + json, + serde_json::json!({ "type": "deductFromInput", "index": 7 }) + ); + } + + #[test] + fn reduce_output_serializes_with_type_and_index() { + let step = AddressFundsFeeStrategyStep::ReduceOutput(3); + let json = serde_json::to_value(&step).unwrap(); + assert_eq!( + json, + serde_json::json!({ "type": "reduceOutput", "index": 3 }) + ); + } + + #[test] + fn deserializes_from_type_and_index() { + let step: AddressFundsFeeStrategyStep = + serde_json::from_value(serde_json::json!({ "type": "deductFromInput", "index": 9 })) + .unwrap(); + assert_eq!(step, AddressFundsFeeStrategyStep::DeductFromInput(9)); + + let step: AddressFundsFeeStrategyStep = + serde_json::from_value(serde_json::json!({ "type": "reduceOutput", "index": 2 })) + .unwrap(); + assert_eq!(step, AddressFundsFeeStrategyStep::ReduceOutput(2)); + } + + #[test] + fn rejects_unknown_variant() { + let result: Result = + serde_json::from_value(serde_json::json!({ "type": "burn", "index": 0 })); + assert!(result.is_err()); + } + + #[test] + fn round_trips_through_json() { + for original in [ + AddressFundsFeeStrategyStep::DeductFromInput(0), + AddressFundsFeeStrategyStep::DeductFromInput(42), + AddressFundsFeeStrategyStep::ReduceOutput(0), + AddressFundsFeeStrategyStep::ReduceOutput(42), + ] { + let json = serde_json::to_string(&original).unwrap(); + let restored: AddressFundsFeeStrategyStep = serde_json::from_str(&json).unwrap(); + assert_eq!(original, restored); + } + } +} diff --git a/packages/rs-dpp/src/address_funds/mod.rs b/packages/rs-dpp/src/address_funds/mod.rs index 979ef7970ec..7fa05bd1648 100644 --- a/packages/rs-dpp/src/address_funds/mod.rs +++ b/packages/rs-dpp/src/address_funds/mod.rs @@ -2,6 +2,8 @@ pub mod fee_strategy; #[cfg(feature = "shielded-client")] mod orchard_address; mod platform_address; +#[cfg(feature = "json-conversion")] +pub mod serde_helpers; mod witness; mod witness_verification_operations; diff --git a/packages/rs-dpp/src/address_funds/platform_address.rs b/packages/rs-dpp/src/address_funds/platform_address.rs index 933e13f7591..34dcd536593 100644 --- a/packages/rs-dpp/src/address_funds/platform_address.rs +++ b/packages/rs-dpp/src/address_funds/platform_address.rs @@ -35,11 +35,6 @@ pub const ADDRESS_HASH_SIZE: usize = 20; PlatformSerialize, PlatformDeserialize, )] -#[cfg_attr( - feature = "serde-conversion", - derive(Serialize, Deserialize), - serde(rename_all = "camelCase") -)] #[platform_serialize(unversioned)] pub enum PlatformAddress { /// Pay to pubkey hash @@ -52,6 +47,106 @@ pub enum PlatformAddress { P2sh([u8; 20]), } +// Custom serde impls so JSON / `platform_value` output is the canonical 21-byte +// address representation (hex string in human-readable formats, raw bytes in +// binary formats) — matching the wasm wrapper's serde and what consumers expect. +// The `Encode` / `Decode` derives above are the consensus binary format and are +// untouched. +#[cfg(feature = "serde-conversion")] +impl Serialize for PlatformAddress { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let bytes = self.to_bytes(); + if serializer.is_human_readable() { + serializer.serialize_str(&hex::encode(&bytes)) + } else { + serializer.serialize_bytes(&bytes) + } + } +} + +#[cfg(feature = "serde-conversion")] +impl<'de> Deserialize<'de> for PlatformAddress { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{self, Visitor}; + use std::fmt; + + /// Maximum on-the-wire byte length for a `PlatformAddress`: 1 type byte + 20 hash bytes. + const PLATFORM_ADDRESS_BYTE_LEN: usize = 21; + + struct PlatformAddressVisitor; + + impl<'de> Visitor<'de> for PlatformAddressVisitor { + type Value = PlatformAddress; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("PlatformAddress as 21 bytes or hex string") + } + + fn visit_str(self, value: &str) -> Result { + let bytes = + hex::decode(value).map_err(|err| E::custom(format!("invalid hex: {}", err)))?; + if bytes.len() != PLATFORM_ADDRESS_BYTE_LEN { + return Err(E::invalid_length(bytes.len(), &self)); + } + PlatformAddress::from_bytes(&bytes).map_err(|err| E::custom(err.to_string())) + } + + fn visit_string(self, value: String) -> Result { + self.visit_str(&value) + } + + fn visit_bytes(self, value: &[u8]) -> Result { + if value.len() != PLATFORM_ADDRESS_BYTE_LEN { + return Err(E::invalid_length(value.len(), &self)); + } + PlatformAddress::from_bytes(value).map_err(|err| E::custom(err.to_string())) + } + + fn visit_byte_buf(self, value: Vec) -> Result { + self.visit_bytes(&value) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + // Cap at PLATFORM_ADDRESS_BYTE_LEN + 1 so we can detect over-long input + // without allocating arbitrary memory from a malicious peer. + let mut bytes = Vec::with_capacity(PLATFORM_ADDRESS_BYTE_LEN); + while let Some(byte) = seq.next_element::()? { + if bytes.len() >= PLATFORM_ADDRESS_BYTE_LEN { + return Err(de::Error::invalid_length( + bytes.len() + 1, + &"at most 21 bytes", + )); + } + bytes.push(byte); + } + if bytes.len() != PLATFORM_ADDRESS_BYTE_LEN { + return Err(de::Error::invalid_length(bytes.len(), &self)); + } + PlatformAddress::from_bytes(&bytes) + .map_err(|err| de::Error::custom(err.to_string())) + } + } + + // Dispatch on the format's self-description: human-readable formats (JSON, TOML) + // get the hex-string path; binary formats get raw bytes. This avoids the + // `deserialize_any` pitfall on non-self-describing transports. + if deserializer.is_human_readable() { + deserializer.deserialize_str(PlatformAddressVisitor) + } else { + deserializer.deserialize_bytes(PlatformAddressVisitor) + } + } +} + impl TryFrom
for PlatformAddress { type Error = ProtocolError; diff --git a/packages/rs-dpp/src/address_funds/serde_helpers/address_input_map.rs b/packages/rs-dpp/src/address_funds/serde_helpers/address_input_map.rs new file mode 100644 index 00000000000..a72a54afce1 --- /dev/null +++ b/packages/rs-dpp/src/address_funds/serde_helpers/address_input_map.rs @@ -0,0 +1,148 @@ +//! `#[serde(with = "address_input_map")]` helper. +//! +//! Reshapes `BTreeMap` to/from an +//! array of `{ address, nonce, amount }` entries on the JSON / Object wire. + +use crate::address_funds::PlatformAddress; +use crate::fee::Credits; +use crate::prelude::AddressNonce; +use crate::serialization::json_safe_fields; +use serde::de::{self, SeqAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::BTreeMap; +use std::fmt; + +#[json_safe_fields] +#[derive(Serialize, Deserialize)] +struct AddressInputEntry { + address: PlatformAddress, + nonce: AddressNonce, + amount: Credits, +} + +pub fn serialize( + map: &BTreeMap, + serializer: S, +) -> Result +where + S: Serializer, +{ + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(map.len()))?; + for (address, (nonce, amount)) in map { + seq.serialize_element(&AddressInputEntry { + address: *address, + nonce: *nonce, + amount: *amount, + })?; + } + seq.end() +} + +pub fn deserialize<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_seq(AddressInputMapVisitor) +} + +struct AddressInputMapVisitor; + +impl<'de> Visitor<'de> for AddressInputMapVisitor { + type Value = BTreeMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an array of { address, nonce, amount } objects") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut map = BTreeMap::new(); + while let Some(entry) = seq.next_element::()? { + if map + .insert(entry.address, (entry.nonce, entry.amount)) + .is_some() + { + return Err(de::Error::custom(format!( + "duplicate input address: {}", + hex::encode(entry.address.to_bytes()) + ))); + } + } + Ok(map) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn p2pkh(byte: u8) -> PlatformAddress { + PlatformAddress::P2pkh([byte; 20]) + } + + /// Newtype wrapper so we can drive the helper through a serde derive. + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Wrapper(#[serde(with = "super")] BTreeMap); + + #[test] + fn empty_round_trips() { + let original = Wrapper(BTreeMap::new()); + let json = serde_json::to_string(&original).expect("serialize empty map"); + assert_eq!(json, "[]"); + let restored: Wrapper = serde_json::from_str(&json).expect("deserialize empty array"); + assert_eq!(original, restored); + } + + #[test] + fn single_entry_round_trips() { + let mut map = BTreeMap::new(); + map.insert(p2pkh(1), (5u32, 1_000u64)); + let original = Wrapper(map); + + let value = serde_json::to_value(&original).expect("serialize single entry"); + assert_eq!( + value, + serde_json::json!([ + { "address": "00".to_string() + &"01".repeat(20), "nonce": 5, "amount": 1000 } + ]) + ); + + let restored: Wrapper = serde_json::from_value(value).expect("deserialize single entry"); + assert_eq!(original, restored); + } + + #[test] + fn multiple_entries_emit_in_sorted_address_order() { + let mut map = BTreeMap::new(); + map.insert(p2pkh(2), (2u32, 200u64)); + map.insert(p2pkh(1), (1u32, 100u64)); + map.insert(p2pkh(3), (3u32, 300u64)); + + let original = Wrapper(map); + let value = serde_json::to_value(&original).expect("serialize multi entry"); + let arr = value.as_array().expect("emitted JSON array"); + let nonces: Vec = arr + .iter() + .map(|entry| entry["nonce"].as_u64().expect("nonce as u64")) + .collect(); + assert_eq!(nonces, vec![1, 2, 3]); + + let restored: Wrapper = serde_json::from_value(value).expect("deserialize multi entry"); + assert_eq!(original, restored); + } + + #[test] + fn rejects_duplicate_addresses() { + let json = serde_json::json!([ + { "address": "00".to_string() + &"01".repeat(20), "nonce": 1, "amount": 100 }, + { "address": "00".to_string() + &"01".repeat(20), "nonce": 2, "amount": 200 } + ]); + let result: Result = serde_json::from_value(json); + assert!(result.is_err()); + } +} diff --git a/packages/rs-dpp/src/address_funds/serde_helpers/address_output_map_optional_amount.rs b/packages/rs-dpp/src/address_funds/serde_helpers/address_output_map_optional_amount.rs new file mode 100644 index 00000000000..7a0843a9aa7 --- /dev/null +++ b/packages/rs-dpp/src/address_funds/serde_helpers/address_output_map_optional_amount.rs @@ -0,0 +1,112 @@ +//! `#[serde(with = "address_output_map_optional_amount")]` helper. +//! +//! Reshapes `BTreeMap>` to/from an array of +//! `{ address, amount }` entries (where `amount` may be null) on the JSON / +//! Object wire. + +use crate::address_funds::PlatformAddress; +use crate::fee::Credits; +use crate::serialization::json_safe_fields; +use serde::de::{self, SeqAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::BTreeMap; +use std::fmt; + +#[json_safe_fields] +#[derive(Serialize, Deserialize)] +struct AddressOutputOptionalEntry { + address: PlatformAddress, + amount: Option, +} + +pub fn serialize( + map: &BTreeMap>, + serializer: S, +) -> Result +where + S: Serializer, +{ + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(map.len()))?; + for (address, amount) in map { + seq.serialize_element(&AddressOutputOptionalEntry { + address: *address, + amount: *amount, + })?; + } + seq.end() +} + +pub fn deserialize<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_seq(AddressOutputOptionalMapVisitor) +} + +struct AddressOutputOptionalMapVisitor; + +impl<'de> Visitor<'de> for AddressOutputOptionalMapVisitor { + type Value = BTreeMap>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an array of { address, amount } objects (amount may be null)") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut map = BTreeMap::new(); + while let Some(entry) = seq.next_element::()? { + if map.insert(entry.address, entry.amount).is_some() { + return Err(de::Error::custom(format!( + "duplicate output address: {}", + hex::encode(entry.address.to_bytes()) + ))); + } + } + Ok(map) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn p2pkh(byte: u8) -> PlatformAddress { + PlatformAddress::P2pkh([byte; 20]) + } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Wrapper(#[serde(with = "super")] BTreeMap>); + + #[test] + fn round_trip_with_some_and_none_amounts() { + let mut map = BTreeMap::new(); + map.insert(p2pkh(1), Some(500u64)); + map.insert(p2pkh(2), None); + + let original = Wrapper(map); + let value = serde_json::to_value(&original).expect("serialize map"); + let arr = value.as_array().expect("emitted JSON array"); + assert_eq!(arr.len(), 2); + assert_eq!(arr[0]["amount"], serde_json::json!(500)); + assert_eq!(arr[1]["amount"], serde_json::Value::Null); + + let restored: Wrapper = serde_json::from_value(value).expect("deserialize map"); + assert_eq!(original, restored); + } + + #[test] + fn rejects_duplicate_addresses() { + let json = serde_json::json!([ + { "address": "00".to_string() + &"01".repeat(20), "amount": 1 }, + { "address": "00".to_string() + &"01".repeat(20), "amount": 2 } + ]); + let result: Result = serde_json::from_value(json); + assert!(result.is_err()); + } +} diff --git a/packages/rs-dpp/src/address_funds/serde_helpers/address_output_map_required_amount.rs b/packages/rs-dpp/src/address_funds/serde_helpers/address_output_map_required_amount.rs new file mode 100644 index 00000000000..91016902009 --- /dev/null +++ b/packages/rs-dpp/src/address_funds/serde_helpers/address_output_map_required_amount.rs @@ -0,0 +1,103 @@ +//! `#[serde(with = "address_output_map_required_amount")]` helper. +//! +//! Reshapes `BTreeMap` to/from an array of +//! `{ address, amount }` entries on the JSON / Object wire. The `amount` field +//! is always required and never serializes as `null`. + +use super::AddressOutputEntry; +use crate::address_funds::PlatformAddress; +use crate::fee::Credits; +use serde::de::{self, SeqAccess, Visitor}; +use serde::{Deserializer, Serializer}; +use std::collections::BTreeMap; +use std::fmt; + +pub fn serialize( + map: &BTreeMap, + serializer: S, +) -> Result +where + S: Serializer, +{ + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(map.len()))?; + for (address, amount) in map { + seq.serialize_element(&AddressOutputEntry { + address: *address, + amount: *amount, + })?; + } + seq.end() +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_seq(AddressOutputMapVisitor) +} + +struct AddressOutputMapVisitor; + +impl<'de> Visitor<'de> for AddressOutputMapVisitor { + type Value = BTreeMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an array of { address, amount } objects with required amount") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut map = BTreeMap::new(); + while let Some(entry) = seq.next_element::()? { + if map.insert(entry.address, entry.amount).is_some() { + return Err(de::Error::custom(format!( + "duplicate output address: {}", + hex::encode(entry.address.to_bytes()) + ))); + } + } + Ok(map) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + + fn p2pkh(byte: u8) -> PlatformAddress { + PlatformAddress::P2pkh([byte; 20]) + } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Wrapper(#[serde(with = "super")] BTreeMap); + + #[test] + fn round_trip_required_amount() { + let mut map = BTreeMap::new(); + map.insert(p2pkh(1), 100u64); + map.insert(p2pkh(2), 200u64); + + let original = Wrapper(map); + let value = serde_json::to_value(&original).expect("serialize map"); + let arr = value.as_array().expect("emitted JSON array"); + assert_eq!(arr.len(), 2); + assert_eq!(arr[0]["amount"], serde_json::json!(100)); + assert_eq!(arr[1]["amount"], serde_json::json!(200)); + + let restored: Wrapper = serde_json::from_value(value).expect("deserialize map"); + assert_eq!(original, restored); + } + + #[test] + fn rejects_missing_amount() { + let json = serde_json::json!([ + { "address": "00".to_string() + &"01".repeat(20) } + ]); + let result: Result = serde_json::from_value(json); + assert!(result.is_err()); + } +} diff --git a/packages/rs-dpp/src/address_funds/serde_helpers/address_output_singular.rs b/packages/rs-dpp/src/address_funds/serde_helpers/address_output_singular.rs new file mode 100644 index 00000000000..19154706991 --- /dev/null +++ b/packages/rs-dpp/src/address_funds/serde_helpers/address_output_singular.rs @@ -0,0 +1,69 @@ +//! `#[serde(with = "address_output_singular")]` helper. +//! +//! Reshapes `Option<(PlatformAddress, Credits)>` to/from a single +//! `{ address, amount }` object (or `null`) on the JSON / Object wire. + +use super::AddressOutputEntry; +use crate::address_funds::PlatformAddress; +use crate::fee::Credits; +use serde::{Deserialize, Deserializer, Serializer}; + +pub fn serialize( + value: &Option<(PlatformAddress, Credits)>, + serializer: S, +) -> Result +where + S: Serializer, +{ + match value { + Some((address, amount)) => serializer.serialize_some(&AddressOutputEntry { + address: *address, + amount: *amount, + }), + None => serializer.serialize_none(), + } +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt: Option = Option::deserialize(deserializer)?; + Ok(opt.map(|entry| (entry.address, entry.amount))) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Serialize; + + fn p2pkh(byte: u8) -> PlatformAddress { + PlatformAddress::P2pkh([byte; 20]) + } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Wrapper(#[serde(with = "super")] Option<(PlatformAddress, Credits)>); + + #[test] + fn none_serializes_to_null() { + let original = Wrapper(None); + let value = serde_json::to_value(&original).expect("serialize None"); + assert_eq!(value, serde_json::Value::Null); + + let restored: Wrapper = serde_json::from_value(value).expect("deserialize null"); + assert_eq!(original, restored); + } + + #[test] + fn some_round_trips_as_object() { + let original = Wrapper(Some((p2pkh(7), 42u64))); + let value = serde_json::to_value(&original).expect("serialize Some"); + assert_eq!(value["amount"], serde_json::json!(42)); + let address_hex = value["address"].as_str().expect("address as hex string"); + assert!(address_hex.starts_with("00")); // P2pkh discriminant + assert_eq!(address_hex.len(), 42); // 21 bytes hex-encoded + + let restored: Wrapper = serde_json::from_value(value).expect("deserialize object"); + assert_eq!(original, restored); + } +} diff --git a/packages/rs-dpp/src/address_funds/serde_helpers/mod.rs b/packages/rs-dpp/src/address_funds/serde_helpers/mod.rs new file mode 100644 index 00000000000..b6ac51a7ff0 --- /dev/null +++ b/packages/rs-dpp/src/address_funds/serde_helpers/mod.rs @@ -0,0 +1,41 @@ +//! Field-level serde helpers for address-based transition fields. +//! +//! These helpers reshape the JSON / wasm Object output of `BTreeMap` +//! and `Option<(PlatformAddress, _)>` fields from an opaque map-of-tuples into a +//! self-describing array (or single object) of `{ address, nonce?, amount? }` entries. +//! +//! Only serde JSON / `platform_value` output is affected — the bincode `Encode` / +//! `Decode` derives on the parent transitions are independent of serde and remain +//! unchanged, so consensus binary format and `PlatformSignable` sighash are +//! intentionally untouched. Same safety argument as the custom-serde change applied +//! to `AddressFundsFeeStrategyStep`. +//! +//! Each helper exposes `pub fn serialize` and `pub fn deserialize` so it can be +//! attached to a struct field via `#[serde(with = "...")]`. +//! +//! Module gating lives on the parent re-export in `address_funds/mod.rs` +//! (`#[cfg(feature = "json-conversion")]`), so this file does not need its +//! own inner `#![cfg(...)]` attribute. + +use crate::address_funds::PlatformAddress; +use crate::fee::Credits; +use crate::serialization::json_safe_fields; +use serde::{Deserialize, Serialize}; + +pub mod address_input_map; +pub mod address_output_map_optional_amount; +pub mod address_output_map_required_amount; +pub mod address_output_singular; + +/// Wire shape for an output address entry: `{ address, amount }` with required amount. +/// Shared between the `address_output_map_required_amount` and `address_output_singular` +/// helpers so the JSON shape stays consistent across plural / singular fields. +/// +/// `#[json_safe_fields]` auto-applies `json_safe_u64` to the `amount` field so values +/// above `Number.MAX_SAFE_INTEGER` (2^53 − 1) are stringified in human-readable JSON. +#[json_safe_fields] +#[derive(Serialize, Deserialize)] +pub(crate) struct AddressOutputEntry { + pub(crate) address: PlatformAddress, + pub(crate) amount: Credits, +} diff --git a/packages/rs-dpp/src/identity/core_script.rs b/packages/rs-dpp/src/identity/core_script.rs index 7ae376a041c..e9cc9848c7e 100644 --- a/packages/rs-dpp/src/identity/core_script.rs +++ b/packages/rs-dpp/src/identity/core_script.rs @@ -157,34 +157,47 @@ impl<'de> Deserialize<'de> for CoreScript { where D: serde::Deserializer<'de>, { - if deserializer.is_human_readable() { - let data: String = Deserialize::deserialize(deserializer)?; - - Self::from_string(&data, Encoding::Base64).map_err(|e| { - serde::de::Error::custom(format!( - "expected to be able to deserialize core script from string: {}", - e - )) - }) - } else { - struct BytesVisitor; + // Both visitors accept strings AND bytes regardless of the deserializer's + // human-readable flag — same pattern as `BinaryData` / `Identifier`. This + // covers the case where serde-wasm-bindgen emits a `Uint8Array` for the + // Object form (which the deserializer reports as human-readable), and the + // case where a JSON consumer sends a base64 string through the binary path. - impl Visitor<'_> for BytesVisitor { - type Value = CoreScript; + struct CoreScriptVisitor; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a byte array") - } + impl Visitor<'_> for CoreScriptVisitor { + type Value = CoreScript; - fn visit_bytes(self, v: &[u8]) -> Result - where - E: serde::de::Error, - { - Ok(CoreScript::from_bytes(v.to_vec())) - } + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array or base64-encoded string") + } + + fn visit_str(self, v: &str) -> Result { + CoreScript::from_string(v, Encoding::Base64).map_err(|e| { + E::custom(format!( + "expected to be able to deserialize core script from string: {}", + e + )) + }) + } + + fn visit_string(self, v: String) -> Result { + self.visit_str(&v) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(CoreScript::from_bytes(v.to_vec())) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(CoreScript::from_bytes(v)) } + } - deserializer.deserialize_bytes(BytesVisitor) + if deserializer.is_human_readable() { + deserializer.deserialize_string(CoreScriptVisitor) + } else { + deserializer.deserialize_bytes(CoreScriptVisitor) } } } diff --git a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs index 822981da827..98e4fa5696b 100644 --- a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs +++ b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs @@ -24,16 +24,29 @@ pub mod validate_asset_lock_transaction_structure; // TODO: Serialization with bincode // TODO: Consider use Box for InstantAssetLockProof +// +// Wire-shape note: this is an *internally-tagged* enum (`#[serde(tag = "type")]` +// with no `content`). serde's internal tagging works on newtype variants whose +// inner is a struct — both `InstantAssetLockProof` and `ChainAssetLockProof` +// qualify — so the inner struct's fields are flattened next to the `type` +// discriminator: `{"type": "instant", "instantLock": ..., "transaction": ..., +// "outputIndex": ...}`. This matches the convention applied to other tagged +// unions exposed to JS (see `AddressWitness`, `AddressFundsFeeStrategyStep`). +// Bincode `Encode`/`Decode` derives are independent of serde, so consensus +// binary format is unaffected. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Encode, Decode)] -#[serde(untagged)] +#[serde(tag = "type", rename_all = "camelCase")] #[allow(clippy::large_enum_variant)] pub enum AssetLockProof { Instant(#[bincode(with_serde)] InstantAssetLockProof), Chain(#[bincode(with_serde)] ChainAssetLockProof), } +/// Wire-shape Deserialize uses the same internal-tag layout the Serialize derive +/// produces, but routes the instant variant through `RawInstantLockProof` so the +/// dashcore `InstantLock` can be reconstructed from its raw bytes form. #[derive(Deserialize)] -#[serde(untagged)] +#[serde(tag = "type", rename_all = "camelCase")] enum RawAssetLockProof { Instant(RawInstantLockProof), Chain(ChainAssetLockProof), @@ -59,21 +72,6 @@ impl<'de> Deserialize<'de> for AssetLockProof { where D: Deserializer<'de>, { - // Try to parse into IS Lock - // let maybe_is_lock = RawInstantLock::deserialize(&deserializer); - // - // if let Ok(raw_instant_lock) = maybe_is_lock { - // let instant_lock = raw_instant_lock.try_into() - // .map_err(|e: ProtocolError| D::Error::custom(e.to_string()))?; - // - // return Ok(AssetLockProof::Instant(instant_lock)) - // }; - // - // - // ChainAssetLockProof::deserialize(deserializer) - // .map(|chain| AssetLockProof::Chain(chain)) - // // Try to parse into chain lock - let raw = RawAssetLockProof::deserialize(deserializer)?; raw.try_into().map_err(|e: ProtocolError| { D::Error::custom(format!( @@ -95,43 +93,6 @@ impl AsRef for AssetLockProof { self } } -// -// impl Serialize for AssetLockProof { -// fn serialize(&self, serializer: S) -> Result -// where -// S: Serializer, -// { -// match self { -// AssetLockProof::Instant(instant_proof) => instant_proof.serialize(serializer), -// AssetLockProof::Chain(chain) => chain.serialize(serializer), -// } -// } -// } -// -// impl<'de> Deserialize<'de> for AssetLockProof { -// fn deserialize(deserializer: D) -> Result -// where -// D: Deserializer<'de>, -// { -// let value = platform_value::Value::deserialize(deserializer)?; -// -// let proof_type_int: u8 = value -// .get_integer("type") -// .map_err(|e| D::Error::custom(e.to_string()))?; -// let proof_type = AssetLockProofType::try_from(proof_type_int) -// .map_err(|e| D::Error::custom(e.to_string()))?; -// -// match proof_type { -// AssetLockProofType::Instant => Ok(Self::Instant( -// platform_value::from_value(value).map_err(|e| D::Error::custom(e.to_string()))?, -// )), -// AssetLockProofType::Chain => Ok(Self::Chain( -// platform_value::from_value(value).map_err(|e| D::Error::custom(e.to_string()))?, -// )), -// } -// } -// } - pub enum AssetLockProofType { Instant = 0, Chain = 1, @@ -332,6 +293,37 @@ impl TryInto for &AssetLockProof { mod tests { use super::*; use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + use dashcore::{OutPoint, Txid}; + use std::str::FromStr; + + /// JSON wire shape is internally tagged: `{type, ...flattened inner fields}`, + /// no `data` wrapper. This guards against accidental reintroduction of the + /// old adjacent-tagged `{type, data: {...}}` shape and against the divergence + /// from the `AddressWitness` / `AddressFundsFeeStrategyStep` precedent. + #[test] + fn chain_variant_serializes_with_internal_tag() { + let txid = + Txid::from_str("e8b43025641eea4fd21190f01bd870ef90f1a8b199d8fc3376c5b62c0b1a179d") + .unwrap(); + let proof = AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 11, + out_point: OutPoint { txid, vout: 1 }, + }); + + let json = serde_json::to_value(&proof).expect("serialize"); + + assert_eq!(json["type"], "chain"); + assert_eq!(json["coreChainLockedHeight"], 11); + assert!( + json.get("data").is_none(), + "should not have a `data` wrapper, got: {}", + json + ); + + // Round-trip + let restored: AssetLockProof = serde_json::from_value(json).expect("deserialize"); + assert_eq!(proof, restored); + } mod asset_lock_proof_type_try_from { use super::*; diff --git a/packages/rs-dpp/src/identity/v0/conversion/json.rs b/packages/rs-dpp/src/identity/v0/conversion/json.rs index e9f0b323fd8..aba1889abd8 100644 --- a/packages/rs-dpp/src/identity/v0/conversion/json.rs +++ b/packages/rs-dpp/src/identity/v0/conversion/json.rs @@ -83,25 +83,23 @@ mod tests { assert!(obj.contains_key("revision")); } - // frozen: V0 consensus behavior + // V0 to_json -> from_json round-trip succeeds. // - // `IdentityV0::to_json` produces a JSON form where the inner public keys carry the - // `$formatVersion` serde-adjacency tag and `data` is base64-encoded; but - // `IdentityV0::from_json` does the reverse mapping via `replace_at_paths` + - // `platform_value::from_value`. That combination does not round-trip for `IdentityV0` - // because the inner platform_value deserializer is inconsistent about - // `is_human_readable()` for nested BinaryData fields. Lock the observed failure in - // so the roundtrip pattern is not silently "fixed" in V0. + // Previously, this combination failed because `BinaryData::Deserialize` was + // asymmetric — its human-readable visitor accepted only strings and the binary + // visitor accepted only byte sequences, while `platform_value`'s nested + // deserializers default to `is_human_readable() = true` even when the value + // carries `Value::Bytes`. The fix in PR #3235 made `BinaryData::Deserialize` + // symmetric (accepts both strings and bytes regardless of mode, mirroring the + // `Identifier` pattern). Bincode `Encode`/`Decode` derives are untouched, so + // consensus binary format is unchanged — only the serde JSON path now + // round-trips cleanly. #[test] - fn to_json_then_from_json_fails_binary_data_roundtrip_v0_frozen() { + fn to_json_then_from_json_round_trips_v0() { let id = sample_identity_v0(); let json = id.to_json().unwrap(); - let back = IdentityV0::from_json(json); - assert!( - back.is_err(), - "V0 to_json -> from_json roundtrip is not expected to succeed; \ - if this starts to pass, V0 consensus behavior may have changed" - ); + let back = IdentityV0::from_json(json).expect("v0 round-trip should succeed"); + assert_eq!(id, back); } #[test] diff --git a/packages/rs-dpp/src/identity/v0/conversion/platform_value.rs b/packages/rs-dpp/src/identity/v0/conversion/platform_value.rs index 972cae1fad5..30b25792c68 100644 --- a/packages/rs-dpp/src/identity/v0/conversion/platform_value.rs +++ b/packages/rs-dpp/src/identity/v0/conversion/platform_value.rs @@ -96,30 +96,29 @@ mod tests { assert_eq!(object, cleaned); } - // frozen: V0 consensus behavior + // V0 to_object -> TryFrom round-trip succeeds. // - // `IdentityV0::to_object()` (from the `ValueConvertible` derive) serializes through - // platform_value's non-human-readable path and encodes `BinaryData` as `Value::Bytes`. - // But `platform_value::from_value(...)` produces inner deserializers that default to - // `is_human_readable() = true`, so `BinaryData::deserialize` dispatches to its string - // visitor and fails on `Value::Bytes`. The direct round-trip therefore does NOT work; - // consumers must go through the explicit conversion helpers (JSON path, etc.). + // Previously this failed because of an asymmetric `BinaryData::Deserialize` + // (string-only in human-readable mode, bytes-only in binary mode), while + // `platform_value`'s nested deserializers default to + // `is_human_readable() = true` even when the value carries `Value::Bytes`. + // The fix in PR #3235 made `BinaryData::Deserialize` symmetric (accepts + // both strings and bytes regardless of mode, mirroring `Identifier`). + // Bincode `Encode`/`Decode` derives are untouched, so consensus binary + // format is unchanged — only the serde platform_value path now round-trips. #[test] - fn to_object_then_try_from_fails_v0_frozen() { + fn to_object_then_try_from_round_trips_v0() { let id = sample_with_disabled(Some(9)); let value = id.to_object().unwrap(); - let result = IdentityV0::try_from(value); - assert!( - result.is_err(), - "V0 to_object -> TryFrom round-trip is not expected to succeed" - ); + let back = IdentityV0::try_from(value).expect("v0 round-trip should succeed"); + assert_eq!(id, back); } #[test] - fn try_from_ref_value_fails_on_to_object_output_v0_frozen() { + fn try_from_ref_value_round_trips_v0() { let id = sample_with_disabled(None); let value = id.to_object().unwrap(); - let result = IdentityV0::try_from(&value); - assert!(result.is_err()); + let back = IdentityV0::try_from(&value).expect("v0 round-trip should succeed"); + assert_eq!(id, back); } } diff --git a/packages/rs-dpp/src/serialization/json/safe_fields.rs b/packages/rs-dpp/src/serialization/json/safe_fields.rs index 2c121f61e68..6468a58af2f 100644 --- a/packages/rs-dpp/src/serialization/json/safe_fields.rs +++ b/packages/rs-dpp/src/serialization/json/safe_fields.rs @@ -93,6 +93,8 @@ impl JsonSafeFields for crate::block::epoch::Epoch {} impl JsonSafeFields for crate::identity::identity_public_key::IdentityPublicKey {} impl JsonSafeFields for crate::identity::state_transition::asset_lock_proof::AssetLockProof {} impl JsonSafeFields for crate::address_funds::PlatformAddress {} +impl JsonSafeFields for crate::address_funds::AddressFundsFeeStrategy {} +impl JsonSafeFields for crate::address_funds::AddressWitness {} impl JsonSafeFields for crate::withdrawal::Pooling {} impl JsonSafeFields for crate::identity::core_script::CoreScript {} impl JsonSafeFields for crate::voting::votes::Vote {} diff --git a/packages/rs-dpp/src/serialization/mod.rs b/packages/rs-dpp/src/serialization/mod.rs index b7b39491538..aa97ea9fa6c 100644 --- a/packages/rs-dpp/src/serialization/mod.rs +++ b/packages/rs-dpp/src/serialization/mod.rs @@ -1,7 +1,9 @@ #[cfg(feature = "json-conversion")] pub mod json; #[cfg(feature = "serde-conversion")] -pub(crate) mod serde_bytes_64; +pub mod serde_bytes; +#[cfg(feature = "serde-conversion")] +pub mod serde_bytes_var; pub(crate) mod serialization_traits; pub use dpp_json_convertible_derive::json_safe_fields; diff --git a/packages/rs-dpp/src/serialization/serde_bytes.rs b/packages/rs-dpp/src/serialization/serde_bytes.rs new file mode 100644 index 00000000000..1030d300684 --- /dev/null +++ b/packages/rs-dpp/src/serialization/serde_bytes.rs @@ -0,0 +1,146 @@ +//! Generic serde helper for fixed-size byte arrays `[u8; N]`. +//! +//! Default serde serializes `[u8; N]` for N ≤ 32 as a tuple of u8 elements +//! (a sequence of numbers in JSON, opaque in non-self-describing formats). +//! For N > 32 there is no default impl at all. +//! +//! This module gives a single, length-agnostic shape: +//! +//! - **Human-readable** formats (JSON): base64-encoded string (matches +//! `Bytes20` / `Bytes32` / `Bytes36` / `BinaryData` in `rs-platform-value`) +//! - **Binary** formats (bincode, CBOR, `platform_value`): raw byte sequence +//! (which becomes `Uint8Array` through `serde_wasm_bindgen` with +//! `serialize_bytes_as_arrays(false)`) +//! +//! Used via `#[serde(with = "crate::serialization::serde_bytes")]` on any +//! `[u8; N]` field. The `#[json_safe_fields]` proc-macro injects this for +//! every fixed-size byte field. + +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use serde::de::{self, SeqAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serializer}; +use std::fmt; + +pub fn serialize( + bytes: &[u8; N], + serializer: S, +) -> Result { + if serializer.is_human_readable() { + serializer.serialize_str(&BASE64_STANDARD.encode(bytes)) + } else { + serializer.serialize_bytes(bytes) + } +} + +pub fn deserialize<'de, D: Deserializer<'de>, const N: usize>( + deserializer: D, +) -> Result<[u8; N], D::Error> { + if deserializer.is_human_readable() { + let s = ::deserialize(deserializer)?; + let vec = BASE64_STANDARD + .decode(&s) + .map_err(serde::de::Error::custom)?; + vec.try_into().map_err(|v: Vec| { + serde::de::Error::custom(format!("expected {} bytes, got {}", N, v.len())) + }) + } else { + // Accept both byte-buffer formats (`serde_wasm_bindgen` Uint8Array, + // `platform_value::Value::Bytes` → `visit_bytes` / `visit_byte_buf`) + // and length-prefixed sequences (bincode → `visit_seq`). Going through + // `>::deserialize` would only cover the seq path. + struct BytesOrSeqVisitor; + + impl<'de, const N: usize> Visitor<'de> for BytesOrSeqVisitor { + type Value = [u8; N]; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} bytes (as a byte buffer or sequence of u8)", N) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + v.try_into() + .map_err(|_| E::custom(format!("expected {} bytes, got {}", N, v.len()))) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + let len = v.len(); + v.try_into() + .map_err(|_| E::custom(format!("expected {} bytes, got {}", N, len))) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut buf = Vec::with_capacity(N); + while let Some(b) = seq.next_element::()? { + buf.push(b); + } + let len = buf.len(); + buf.try_into() + .map_err(|_| de::Error::custom(format!("expected {} bytes, got {}", N, len))) + } + } + + deserializer.deserialize_byte_buf(BytesOrSeqVisitor::) + } +} + +#[cfg(test)] +mod tests { + use base64::prelude::BASE64_STANDARD; + use base64::Engine; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Wrap32(#[serde(with = "super")] [u8; 32]); + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Wrap64(#[serde(with = "super")] [u8; 64]); + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Wrap20(#[serde(with = "super")] [u8; 20]); + + #[test] + fn json_round_trip_32_bytes_uses_base64_string() { + let original = Wrap32([0xab; 32]); + let value = serde_json::to_value(&original).expect("serialize"); + assert_eq!(value, serde_json::json!(BASE64_STANDARD.encode([0xab; 32]))); + let restored: Wrap32 = serde_json::from_value(value).expect("deserialize"); + assert_eq!(original, restored); + } + + #[test] + fn json_round_trip_64_bytes_uses_base64_string() { + let original = Wrap64([0xcd; 64]); + let value = serde_json::to_value(&original).expect("serialize"); + assert_eq!(value, serde_json::json!(BASE64_STANDARD.encode([0xcd; 64]))); + let restored: Wrap64 = serde_json::from_value(value).expect("deserialize"); + assert_eq!(original, restored); + } + + #[test] + fn json_round_trip_20_bytes_works_with_const_generic() { + let original = Wrap20([0x12; 20]); + let value = serde_json::to_value(&original).expect("serialize"); + assert_eq!(value, serde_json::json!(BASE64_STANDARD.encode([0x12; 20]))); + let restored: Wrap20 = serde_json::from_value(value).expect("deserialize"); + assert_eq!(original, restored); + } + + #[test] + fn rejects_wrong_length_base64() { + let result: Result = + serde_json::from_value(serde_json::json!(BASE64_STANDARD.encode([0u8; 8]))); + assert!(result.is_err()); + } + + #[test] + fn binary_round_trip_uses_raw_bytes() { + let original = Wrap32([0x55; 32]); + let bytes = bincode::serde::encode_to_vec(&original, bincode::config::standard()) + .expect("bincode encode"); + let (restored, _): (Wrap32, usize) = + bincode::serde::decode_from_slice(&bytes, bincode::config::standard()) + .expect("bincode decode"); + assert_eq!(original, restored); + } +} diff --git a/packages/rs-dpp/src/serialization/serde_bytes_64.rs b/packages/rs-dpp/src/serialization/serde_bytes_64.rs deleted file mode 100644 index 6fc830deb30..00000000000 --- a/packages/rs-dpp/src/serialization/serde_bytes_64.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Serde helper for `[u8; 64]` fields. -//! -//! Serde's built-in array support only covers arrays up to 32 elements. -//! This module provides custom serialize/deserialize for 64-byte arrays -//! (e.g. RedPallas binding signatures, spend auth signatures). -//! -//! - **Human-readable** formats (JSON): hex-encoded string -//! - **Binary** formats (bincode, CBOR): raw byte sequence - -use serde::{Deserialize, Deserializer, Serializer}; - -pub fn serialize(bytes: &[u8; 64], serializer: S) -> Result { - if serializer.is_human_readable() { - serializer.serialize_str(&hex::encode(bytes)) - } else { - serializer.serialize_bytes(bytes) - } -} - -pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<[u8; 64], D::Error> { - if deserializer.is_human_readable() { - let s = ::deserialize(deserializer)?; - let vec = hex::decode(&s).map_err(serde::de::Error::custom)?; - vec.try_into().map_err(|v: Vec| { - serde::de::Error::custom(format!("expected 64 bytes, got {}", v.len())) - }) - } else { - let vec = >::deserialize(deserializer)?; - vec.try_into().map_err(|v: Vec| { - serde::de::Error::custom(format!("expected 64 bytes, got {}", v.len())) - }) - } -} diff --git a/packages/rs-dpp/src/serialization/serde_bytes_var.rs b/packages/rs-dpp/src/serialization/serde_bytes_var.rs new file mode 100644 index 00000000000..048f61435b1 --- /dev/null +++ b/packages/rs-dpp/src/serialization/serde_bytes_var.rs @@ -0,0 +1,108 @@ +//! Serde helper for variable-length `Vec` byte fields. +//! +//! Default serde serializes `Vec` as a sequence of u8 elements. For JS / wasm +//! consumers this is verbose and not ergonomic; we want bytes in binary formats +//! and base64 strings in human-readable formats — matching `BinaryData` (the +//! widely-used opaque-bytes wrapper in `rs-platform-value`) and the const-generic +//! `serde_bytes` helper. +//! +//! - **Human-readable** formats (JSON): base64-encoded string +//! - **Binary** formats (bincode / `platform_value`): raw byte sequence (which +//! becomes `Uint8Array` through `serde_wasm_bindgen` with +//! `serialize_bytes_as_arrays(false)`) + +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use serde::de::{self, SeqAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serializer}; +use std::fmt; + +pub fn serialize(bytes: &Vec, serializer: S) -> Result { + if serializer.is_human_readable() { + serializer.serialize_str(&BASE64_STANDARD.encode(bytes)) + } else { + serializer.serialize_bytes(bytes) + } +} + +pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + if deserializer.is_human_readable() { + let s = ::deserialize(deserializer)?; + BASE64_STANDARD.decode(&s).map_err(serde::de::Error::custom) + } else { + // Accept both byte-buffer formats (`serde_wasm_bindgen` Uint8Array → + // `visit_bytes` / `visit_byte_buf`) and length-prefixed sequences + // (bincode, `platform_value::Value::Array(u8)` → `visit_seq`). The + // default `>::deserialize` only covers the seq path. + struct BytesOrSeqVisitor; + + impl<'de> Visitor<'de> for BytesOrSeqVisitor { + type Value = Vec; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("bytes or sequence of u8") + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_vec()) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(v) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(b) = seq.next_element::()? { + bytes.push(b); + } + Ok(bytes) + } + } + + deserializer.deserialize_byte_buf(BytesOrSeqVisitor) + } +} + +#[cfg(test)] +mod tests { + use base64::prelude::BASE64_STANDARD; + use base64::Engine; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Wrap(#[serde(with = "super")] Vec); + + #[test] + fn json_emits_base64_string() { + let original = Wrap(vec![0xde, 0xad, 0xbe, 0xef]); + let value = serde_json::to_value(&original).expect("serialize"); + assert_eq!( + value, + serde_json::json!(BASE64_STANDARD.encode([0xde, 0xad, 0xbe, 0xef])) + ); + + let restored: Wrap = serde_json::from_value(value).expect("deserialize"); + assert_eq!(original, restored); + } + + #[test] + fn empty_vec_round_trips() { + let original = Wrap(Vec::new()); + let value = serde_json::to_value(&original).expect("serialize empty"); + assert_eq!(value, serde_json::json!("")); + let restored: Wrap = serde_json::from_value(value).expect("deserialize empty"); + assert_eq!(original, restored); + } + + #[test] + fn binary_round_trip_uses_raw_bytes() { + let original = Wrap(vec![1, 2, 3, 4, 5]); + let bytes = bincode::serde::encode_to_vec(&original, bincode::config::standard()) + .expect("bincode encode"); + let (restored, _): (Wrap, usize) = + bincode::serde::decode_from_slice(&bytes, bincode::config::standard()) + .expect("bincode decode"); + assert_eq!(original, restored); + } +} diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index bc2db958bfd..56f09b2e2cd 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -99,6 +99,11 @@ pub struct OrchardBundleParams { /// All fields except `spend_auth_sig` are covered by the Orchard bundle commitment /// (BLAKE2b-256 per ZIP-244), which feeds into the platform sighash. The signatures /// and proof are verified separately and are not part of the commitment. +/// `#[json_safe_fields]` auto-injects `#[serde(with = ...)]` on the byte fields: +/// every `[u8; N]` → `serde_bytes` (const-generic), `Vec` → `serde_bytes_var`. +/// Keeps the wire shape (Uint8Array in binary, base64 string in JSON) without +/// per-field annotations. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[derive(Debug, Clone, Encode, Decode, PartialEq)] #[cfg_attr( feature = "serde-conversion", @@ -148,9 +153,5 @@ pub struct SerializedAction { /// value_balance, anchor, and any bound transparent fields). Verified against /// `rk` during batch validation. This prevents replay attacks — a valid /// signature from one transition cannot be reused in another. - #[cfg_attr( - feature = "serde-conversion", - serde(with = "crate::serialization::serde_bytes_64") - )] pub spend_auth_sig: [u8; 64], } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/mod.rs index fe83ea3177e..f5a7f789d41 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/mod.rs @@ -27,11 +27,23 @@ use crate::{identity::core_script::CoreScript, withdrawal::Pooling, ProtocolErro )] #[derive(Default)] pub struct AddressCreditWithdrawalTransitionV0 { + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_input_map") + )] pub inputs: BTreeMap, /// Optional output for change + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_output_singular") + )] pub output: Option<(PlatformAddress, Credits)>, pub fee_strategy: AddressFundsFeeStrategy, pub core_fee_per_byte: u32, + #[cfg_attr( + feature = "serde-conversion", + serde(with = "crate::withdrawal::pooling_serde") + )] pub pooling: Pooling, pub output_script: CoreScript, pub user_fee_increase: UserFeeIncrease, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs index bb0326d0e2a..a251db91b44 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs @@ -44,12 +44,20 @@ mod property_names { pub struct AddressFundingFromAssetLockTransitionV0 { pub asset_lock_proof: AssetLockProof, /// Inputs from existing platform addresses (optional, for combining funds) + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_input_map") + )] pub inputs: BTreeMap, /// Outputs to fund platform addresses. /// - `Some(credits)` = explicit amount to send to this address /// - `None` = this address receives everything remaining after explicit outputs and fees /// Exactly one output must be `None` to receive the remainder /// (ensures full asset lock consumption). + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_output_map_optional_amount") + )] pub outputs: BTreeMap>, pub fee_strategy: AddressFundsFeeStrategy, pub user_fee_increase: UserFeeIncrease, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/mod.rs index 0c2cf94699b..bd469959203 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/mod.rs @@ -37,7 +37,15 @@ use serde::{Deserialize, Serialize}; #[platform_serialize(unversioned)] #[derive(Default)] pub struct AddressFundsTransferTransitionV0 { + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_input_map") + )] pub inputs: BTreeMap, + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_output_map_required_amount") + )] pub outputs: BTreeMap, pub fee_strategy: AddressFundsFeeStrategy, pub user_fee_increase: UserFeeIncrease, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs index 23a610e17b6..2d32a138cbc 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs @@ -9,7 +9,6 @@ mod value_conversion; mod version; use std::collections::BTreeMap; -use std::convert::TryFrom; use bincode::{Decode, Encode}; use platform_serialization_derive::PlatformSignable; @@ -27,8 +26,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(rename_all = "camelCase"), - serde(try_from = "IdentityCreateFromAddressesTransitionV0Inner") + serde(rename_all = "camelCase") )] // There is a problem deriving bincode for a borrowed vector // Hence we set to do it somewhat manually inside the PlatformSignable proc macro @@ -39,8 +37,16 @@ pub struct IdentityCreateFromAddressesTransitionV0 { // When signing, we don't sign the signatures for keys #[platform_signable(into = "Vec")] pub public_keys: Vec, + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_input_map") + )] pub inputs: BTreeMap, /// Optional output to send remaining credits to an address + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_output_singular") + )] pub output: Option<(PlatformAddress, Credits)>, pub fee_strategy: AddressFundsFeeStrategy, pub user_fee_increase: UserFeeIncrease, @@ -48,47 +54,6 @@ pub struct IdentityCreateFromAddressesTransitionV0 { pub input_witnesses: Vec, } -#[cfg_attr( - feature = "serde-conversion", - derive(Deserialize), - serde(rename_all = "camelCase") -)] -struct IdentityCreateFromAddressesTransitionV0Inner { - // Own ST fields - public_keys: Vec, - inputs: BTreeMap, - output: Option<(PlatformAddress, Credits)>, - fee_strategy: AddressFundsFeeStrategy, - user_fee_increase: UserFeeIncrease, - input_witnesses: Vec, -} - -impl TryFrom - for IdentityCreateFromAddressesTransitionV0 -{ - type Error = ProtocolError; - - fn try_from(value: IdentityCreateFromAddressesTransitionV0Inner) -> Result { - let IdentityCreateFromAddressesTransitionV0Inner { - public_keys, - inputs, - output, - fee_strategy, - user_fee_increase, - input_witnesses, - } = value; - - Ok(Self { - public_keys, - inputs, - output, - fee_strategy, - user_fee_increase, - input_witnesses, - }) - } -} - #[cfg(test)] mod tests { use super::*; @@ -358,21 +323,6 @@ mod tests { )); } - #[test] - fn try_from_inner_roundtrip() { - let t = make_valid(); - let inner = IdentityCreateFromAddressesTransitionV0Inner { - public_keys: t.public_keys.clone(), - inputs: t.inputs.clone(), - output: t.output, - fee_strategy: t.fee_strategy.clone(), - user_fee_increase: t.user_fee_increase, - input_witnesses: t.input_witnesses.clone(), - }; - let back = IdentityCreateFromAddressesTransitionV0::try_from(inner).expect("try_from"); - assert_eq!(back, t); - } - #[test] fn into_state_transition_wraps_correctly() { use crate::state_transition::identity_create_from_addresses_transition::IdentityCreateFromAddressesTransition; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs index 6c29c9e005e..b1f611057ff 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs @@ -48,9 +48,7 @@ pub struct IdentityCreditTransferToAddressesTransitionV0 { pub identity_id: Identifier, #[cfg_attr( feature = "json-conversion", - serde( - with = "crate::serialization::json::safe_integer_map::json_safe_generic_u64_value_map" - ) + serde(with = "crate::address_funds::serde_helpers::address_output_map_required_amount") )] pub recipient_addresses: BTreeMap, pub nonce: IdentityNonce, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs index d4c2316b63e..f35c75a7592 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs @@ -35,6 +35,10 @@ pub struct IdentityCreditWithdrawalTransitionV0 { pub identity_id: Identifier, pub amount: u64, pub core_fee_per_byte: u32, + #[cfg_attr( + feature = "serde-conversion", + serde(with = "crate::withdrawal::pooling_serde") + )] pub pooling: Pooling, pub output_script: CoreScript, pub nonce: IdentityNonce, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs index 01204d3b974..7364334be17 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs @@ -36,6 +36,10 @@ pub struct IdentityCreditWithdrawalTransitionV1 { pub identity_id: Identifier, pub amount: u64, pub core_fee_per_byte: u32, + #[cfg_attr( + feature = "serde-conversion", + serde(with = "crate::withdrawal::pooling_serde") + )] pub pooling: Pooling, /// If the send to output script is None, then we send the withdrawal to the address set by core pub output_script: Option, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs index a02dbffed41..9b81be9dc55 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs @@ -28,8 +28,16 @@ use crate::ProtocolError; )] #[derive(Default)] pub struct IdentityTopUpFromAddressesTransitionV0 { + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_input_map") + )] pub inputs: BTreeMap, /// Optional output to send remaining credits to an address + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_output_singular") + )] pub output: Option<(PlatformAddress, Credits)>, pub identity_id: Identifier, pub fee_strategy: AddressFundsFeeStrategy, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs index df067719f97..6c0ff2c8a7a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs @@ -14,6 +14,10 @@ use crate::state_transition::StateTransitionFieldTypes; pub type ShieldFromAssetLockTransitionLatest = ShieldFromAssetLockTransitionV0; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use crate::ProtocolError; use bincode::{Decode, Encode}; use derive_more::From; @@ -37,8 +41,13 @@ use serde::{Deserialize, Serialize}; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(tag = "$version") + serde(tag = "$formatVersion") )] +#[cfg_attr( + all(feature = "json-conversion", feature = "serde-conversion"), + derive(JsonConvertible) +)] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[platform_serialize(unversioned)] //versioned directly, no need to use platform_version #[platform_version_path_bounds( "dpp.state_transition_serialization_versions.shield_from_asset_lock_state_transition" diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs index b0215403a9a..14eb575d586 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs @@ -14,6 +14,7 @@ use platform_value::BinaryData; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[derive( Debug, Clone, @@ -43,10 +44,6 @@ pub struct ShieldFromAssetLockTransitionV0 { /// Halo2 proof bytes pub proof: Vec, /// RedPallas binding signature - #[cfg_attr( - feature = "serde-conversion", - serde(with = "crate::serialization::serde_bytes_64") - )] pub binding_signature: [u8; 64], /// ECDSA signature over the signable bytes (excluded from sig hash) #[platform_signable(exclude_from_sig_hash)] diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/mod.rs index dd47b359691..d04d041261c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/mod.rs @@ -12,6 +12,10 @@ use crate::state_transition::StateTransitionFieldTypes; pub type ShieldTransitionLatest = ShieldTransitionV0; use crate::identity::state_transition::OptionallyAssetLockProved; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use crate::ProtocolError; use bincode::{Decode, Encode}; use derive_more::From; @@ -35,8 +39,13 @@ use serde::{Deserialize, Serialize}; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(tag = "$version") + serde(tag = "$formatVersion") )] +#[cfg_attr( + all(feature = "json-conversion", feature = "serde-conversion"), + derive(JsonConvertible) +)] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[platform_serialize(unversioned)] //versioned directly, no need to use platform_version #[platform_version_path_bounds( "dpp.state_transition_serialization_versions.shield_state_transition" diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/v0/mod.rs index 5394e47e35d..597e4372b48 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/v0/mod.rs @@ -17,6 +17,7 @@ use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize, Plat #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[derive( Debug, Clone, @@ -37,6 +38,10 @@ pub struct ShieldTransitionV0 { /// Address inputs funding the shield (address -> nonce + max contribution). /// The total across all inputs must cover |value_balance| + fees. /// Excess credits remain in the source addresses. + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::address_funds::serde_helpers::address_input_map") + )] pub inputs: BTreeMap, /// Orchard actions (spend-output pairs) pub actions: Vec, @@ -47,10 +52,6 @@ pub struct ShieldTransitionV0 { /// Halo2 proof bytes pub proof: Vec, /// RedPallas binding signature - #[cfg_attr( - feature = "serde-conversion", - serde(with = "crate::serialization::serde_bytes_64") - )] pub binding_signature: [u8; 64], /// Fee payment strategy pub fee_strategy: AddressFundsFeeStrategy, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/mod.rs index b3f9c824712..3c8eb39c6f5 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/mod.rs @@ -13,6 +13,10 @@ use crate::state_transition::StateTransitionFieldTypes; pub type ShieldedTransferTransitionLatest = ShieldedTransferTransitionV0; use crate::identity::state_transition::OptionallyAssetLockProved; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use crate::ProtocolError; use bincode::{Decode, Encode}; use derive_more::From; @@ -36,8 +40,13 @@ use serde::{Deserialize, Serialize}; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(tag = "$version") + serde(tag = "$formatVersion") )] +#[cfg_attr( + all(feature = "json-conversion", feature = "serde-conversion"), + derive(JsonConvertible) +)] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[platform_serialize(unversioned)] //versioned directly, no need to use platform_version #[platform_version_path_bounds( "dpp.state_transition_serialization_versions.shielded_transfer_state_transition" diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/mod.rs index 77606d4c713..2c0cb8406be 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/v0/mod.rs @@ -11,6 +11,7 @@ use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize, Plat #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[derive( Debug, Clone, @@ -37,10 +38,6 @@ pub struct ShieldedTransferTransitionV0 { /// Halo2 proof bytes pub proof: Vec, /// RedPallas binding signature - #[cfg_attr( - feature = "serde-conversion", - serde(with = "crate::serialization::serde_bytes_64") - )] pub binding_signature: [u8; 64], } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/mod.rs index b8ff2afbdd2..2eb7b6b8f45 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/mod.rs @@ -13,6 +13,10 @@ use crate::state_transition::StateTransitionFieldTypes; pub type ShieldedWithdrawalTransitionLatest = ShieldedWithdrawalTransitionV0; use crate::identity::state_transition::OptionallyAssetLockProved; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use crate::ProtocolError; use bincode::{Decode, Encode}; use derive_more::From; @@ -36,8 +40,13 @@ use serde::{Deserialize, Serialize}; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(tag = "$version") + serde(tag = "$formatVersion") )] +#[cfg_attr( + all(feature = "json-conversion", feature = "serde-conversion"), + derive(JsonConvertible) +)] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[platform_serialize(unversioned)] //versioned directly, no need to use platform_version #[platform_version_path_bounds( "dpp.state_transition_serialization_versions.shielded_withdrawal_state_transition" diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/mod.rs index 4599f854c61..1bc9f395e81 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/v0/mod.rs @@ -13,6 +13,7 @@ use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize, Plat #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[derive( Debug, Clone, @@ -39,14 +40,14 @@ pub struct ShieldedWithdrawalTransitionV0 { /// Halo2 proof bytes pub proof: Vec, /// RedPallas binding signature - #[cfg_attr( - feature = "serde-conversion", - serde(with = "crate::serialization::serde_bytes_64") - )] pub binding_signature: [u8; 64], /// Core transaction fee rate pub core_fee_per_byte: u32, /// Withdrawal pooling strategy + #[cfg_attr( + feature = "serde-conversion", + serde(with = "crate::withdrawal::pooling_serde") + )] pub pooling: Pooling, /// Core address receiving withdrawn funds pub output_script: CoreScript, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/mod.rs index b8025c9598b..02d8aac6c19 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/mod.rs @@ -13,6 +13,10 @@ use crate::state_transition::StateTransitionFieldTypes; pub type UnshieldTransitionLatest = UnshieldTransitionV0; use crate::identity::state_transition::OptionallyAssetLockProved; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use crate::ProtocolError; use bincode::{Decode, Encode}; use derive_more::From; @@ -36,8 +40,13 @@ use serde::{Deserialize, Serialize}; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(tag = "$version") + serde(tag = "$formatVersion") )] +#[cfg_attr( + all(feature = "json-conversion", feature = "serde-conversion"), + derive(JsonConvertible) +)] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[platform_serialize(unversioned)] //versioned directly, no need to use platform_version #[platform_version_path_bounds( "dpp.state_transition_serialization_versions.unshield_state_transition" diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/mod.rs index e9bffbcc064..0ee73864934 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/v0/mod.rs @@ -12,6 +12,7 @@ use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize, Plat #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[derive( Debug, Clone, @@ -40,10 +41,6 @@ pub struct UnshieldTransitionV0 { /// Halo2 proof bytes pub proof: Vec, /// RedPallas binding signature - #[cfg_attr( - feature = "serde-conversion", - serde(with = "crate::serialization::serde_bytes_64") - )] pub binding_signature: [u8; 64], } diff --git a/packages/rs-dpp/src/withdrawal/mod.rs b/packages/rs-dpp/src/withdrawal/mod.rs index 3a4feda9992..8a5fc3a0f5f 100644 --- a/packages/rs-dpp/src/withdrawal/mod.rs +++ b/packages/rs-dpp/src/withdrawal/mod.rs @@ -21,3 +21,136 @@ pub type WithdrawalTransactionIndex = u64; /// Simple type alias for withdrawal transaction with it's index pub type WithdrawalTransactionIndexAndBytes = (WithdrawalTransactionIndex, Vec); + +/// Serde helper for `Pooling` fields exposed through the JS surface. +/// +/// `Pooling` is `#[repr(u8)]` with `Serialize_repr` / `Deserialize_repr`, so the +/// default wire shape is the numeric discriminant (`0`/`1`/`2`). That number +/// leaks into JSON / Object output and makes `XxxJSON.pooling: string` +/// declarations false. The helper switches the **human-readable** path to a +/// camelCase string (`"never"`/`"ifAvailable"`/`"standard"`) while keeping the +/// non-HR path at the original `u8` so bincode (consensus binary format) is +/// untouched. +/// +/// Apply via `#[serde(with = "crate::withdrawal::pooling_serde")]` on the +/// `pooling` field of any state transition that surfaces it to JS. +#[cfg(feature = "serde-conversion")] +pub mod pooling_serde { + use super::Pooling; + use serde::{Deserializer, Serialize, Serializer}; + + pub fn serialize(pooling: &Pooling, serializer: S) -> Result { + if serializer.is_human_readable() { + let name = match pooling { + Pooling::Never => "never", + Pooling::IfAvailable => "ifAvailable", + Pooling::Standard => "standard", + }; + serializer.serialize_str(name) + } else { + (*pooling as u8).serialize(serializer) + } + } + + /// Deserialize accepts both shapes regardless of the deserializer's + /// human-readable flag — mirrors the `BinaryData` / `Identifier` pattern. + /// Necessary because `platform_value::to_value` reports HR=false (emits the + /// numeric discriminant on the way to `JsValue`), but + /// `platform_value::from_value` reports HR=true on the way back. Without + /// dual acceptance, the `fromObject(toObject())` round-trip fails on the + /// `pooling` field. + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + struct PoolingVisitor; + + impl<'de> serde::de::Visitor<'de> for PoolingVisitor { + type Value = Pooling; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("a Pooling variant: 'never'/'ifAvailable'/'standard' or 0/1/2") + } + + fn visit_str(self, v: &str) -> Result { + match v { + "never" | "Never" => Ok(Pooling::Never), + "ifAvailable" | "IfAvailable" | "ifavailable" => Ok(Pooling::IfAvailable), + "standard" | "Standard" => Ok(Pooling::Standard), + other => Err(E::custom(format!( + "unknown pooling variant '{}', expected 'never' | 'ifAvailable' | 'standard'", + other + ))), + } + } + + fn visit_string(self, v: String) -> Result { + self.visit_str(&v) + } + + fn visit_u64(self, v: u64) -> Result { + match v { + 0 => Ok(Pooling::Never), + 1 => Ok(Pooling::IfAvailable), + 2 => Ok(Pooling::Standard), + other => Err(E::custom(format!("unknown pooling discriminant {}", other))), + } + } + + fn visit_i64(self, v: i64) -> Result { + if v < 0 { + return Err(E::custom(format!("negative pooling discriminant {}", v))); + } + self.visit_u64(v as u64) + } + + fn visit_u8(self, v: u8) -> Result { + self.visit_u64(v as u64) + } + } + + if deserializer.is_human_readable() { + deserializer.deserialize_any(PoolingVisitor) + } else { + deserializer.deserialize_u8(PoolingVisitor) + } + } + + #[cfg(test)] + mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Wrap(#[serde(with = "super")] Pooling); + + #[test] + fn json_emits_camelcase_string() { + for (variant, expected) in [ + (Pooling::Never, "\"never\""), + (Pooling::IfAvailable, "\"ifAvailable\""), + (Pooling::Standard, "\"standard\""), + ] { + let json = serde_json::to_string(&Wrap(variant)).expect("serialize"); + assert_eq!(json, expected); + let restored: Wrap = serde_json::from_str(expected).expect("deserialize"); + assert_eq!(restored, Wrap(variant)); + } + } + + #[test] + fn bincode_keeps_u8_discriminant() { + for (variant, expected_u8) in [ + (Pooling::Never, 0), + (Pooling::IfAvailable, 1), + (Pooling::Standard, 2), + ] { + let bytes = + bincode::serde::encode_to_vec(&Wrap(variant), bincode::config::standard()) + .expect("bincode encode"); + assert_eq!(bytes.last(), Some(&expected_u8)); + let (restored, _): (Wrap, usize) = + bincode::serde::decode_from_slice(&bytes, bincode::config::standard()) + .expect("bincode decode"); + assert_eq!(restored, Wrap(variant)); + } + } + } +} diff --git a/packages/rs-platform-value/src/types/binary_data.rs b/packages/rs-platform-value/src/types/binary_data.rs index d3d39defd88..92e79397e31 100644 --- a/packages/rs-platform-value/src/types/binary_data.rs +++ b/packages/rs-platform-value/src/types/binary_data.rs @@ -35,6 +35,15 @@ impl<'de> Deserialize<'de> for BinaryData { where D: serde::Deserializer<'de>, { + // Both visitors accept strings AND bytes regardless of the deserializer's + // human-readable flag. Mirrors the same pattern used by `Identifier` / + // `Bytes32` / etc.: serde's `ContentDeserializer` (used for internally + // tagged enums like `#[serde(tag = "$version")]`) always reports + // `is_human_readable: true`, so bytes can arrive through the string path + // and vice versa. Without this, transitions whose Object form emits a + // `Uint8Array` for a `BinaryData` field (e.g. `signature`) fail to + // round-trip through `fromObject`. + if deserializer.is_human_readable() { struct StringVisitor; @@ -42,7 +51,7 @@ impl<'de> Deserialize<'de> for BinaryData { type Value = BinaryData; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a base64-encoded string") + formatter.write_str("a base64-encoded string or byte array") } fn visit_str(self, v: &str) -> Result @@ -54,6 +63,20 @@ impl<'de> Deserialize<'de> for BinaryData { })?; Ok(BinaryData(bytes)) } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + Ok(BinaryData(v.to_vec())) + } + + fn visit_byte_buf(self, v: Vec) -> Result + where + E: serde::de::Error, + { + Ok(BinaryData(v)) + } } deserializer.deserialize_string(StringVisitor) @@ -64,7 +87,7 @@ impl<'de> Deserialize<'de> for BinaryData { type Value = BinaryData; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a byte array with length 32") + formatter.write_str("a byte array or base64-encoded string") } fn visit_bytes(self, v: &[u8]) -> Result @@ -73,6 +96,23 @@ impl<'de> Deserialize<'de> for BinaryData { { Ok(BinaryData(v.to_vec())) } + + fn visit_byte_buf(self, v: Vec) -> Result + where + E: serde::de::Error, + { + Ok(BinaryData(v)) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let bytes = BASE64_STANDARD.decode(v).map_err(|e| { + E::custom(format!("expected base64 for binary data: {}", e)) + })?; + Ok(BinaryData(bytes)) + } } deserializer.deserialize_bytes(BytesVisitor) @@ -237,4 +277,70 @@ mod tests { let new_id: Identifier = from_value(value).unwrap(); assert_eq!(id, new_id); } + + /// Proves the **non-HR** path: bincode (`is_human_readable() == false`) + /// dispatches `deserialize_bytes` → `visit_bytes`. Same path as + /// `serde_wasm_bindgen::from_value` via the `dashpay/serde-wasm-bindgen` fork. + #[test] + fn binary_data_deserializes_bytes_through_non_human_readable_path() { + let original = BinaryData::new(vec![0xde, 0xad, 0xbe, 0xef]); + + let bytes = bincode::serde::encode_to_vec(&original, bincode::config::standard()) + .expect("bincode encode"); + let (restored, _): (BinaryData, usize) = + bincode::serde::decode_from_slice(&bytes, bincode::config::standard()) + .expect("bincode decode"); + assert_eq!(original, restored); + } + + /// Proves the **nested-deserializer** path: when `BinaryData` lives inside an + /// internally-tagged enum (`#[serde(tag = "type")]`), serde routes the inner + /// field through `serde::__private::de::ContentDeserializer`, which reports + /// `is_human_readable() == true` regardless of the outer format. So + /// `BinaryData::deserialize` takes the HR branch (`deserialize_string`). + /// The question: when the inner content is `Content::Bytes(...)` (because + /// the source `Value` carries `Value::Bytes`), does `deserialize_string` + /// dispatch to `visit_bytes` on `StringVisitor`? + /// + /// This is the path our wasm-dpp2 `fromObject` runs: + /// `JsValue` → `platform_value::Value` (with `Value::Bytes` for byte fields) + /// → `platform_value::from_value` → tagged enum `ContentDeserializer` → + /// `BinaryData::deserialize` (HR=true) → `deserialize_string(StringVisitor)`. + /// + /// If CodeRabbit's concern is correct, this test fails. + #[test] + fn binary_data_deserializes_bytes_through_internally_tagged_enum() { + use crate::Value; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + #[serde(tag = "type", rename_all = "camelCase")] + enum Wrapper { + Variant { signature: BinaryData }, + } + + // Build the tagged-enum shape by hand with a `Value::Bytes` for the + // nested BinaryData field — exactly what platform_value_from_object + // produces when a JS Object's byte field is a `Uint8Array`. + let value = Value::Map(vec![ + (Value::Text("type".into()), Value::Text("variant".into())), + ( + Value::Text("signature".into()), + Value::Bytes(vec![0xde, 0xad, 0xbe, 0xef]), + ), + ]); + + let restored: Wrapper = crate::from_value(value).expect( + "internally-tagged BinaryData should deserialize from Value::Bytes — \ + if this fails, CodeRabbit's `deserialize_string` blocks `visit_bytes` concern \ + is real for nested deserializers (ContentDeserializer reports HR=true).", + ); + + assert_eq!( + restored, + Wrapper::Variant { + signature: BinaryData::new(vec![0xde, 0xad, 0xbe, 0xef]), + } + ); + } } diff --git a/packages/rs-platform-wallet/src/changeset/core_bridge.rs b/packages/rs-platform-wallet/src/changeset/core_bridge.rs index ddfcfe956e0..36a2718e881 100644 --- a/packages/rs-platform-wallet/src/changeset/core_bridge.rs +++ b/packages/rs-platform-wallet/src/changeset/core_bridge.rs @@ -113,13 +113,14 @@ async fn build_core_changeset( ) -> CoreChangeSet { match event { WalletEvent::TransactionDetected { record, .. } => { - let mut cs = CoreChangeSet::default(); // Derive UTXO deltas BEFORE moving the record into `records` // so we still have the per-record borrows. - cs.new_utxos = derive_new_utxos(record); - cs.spent_utxos = derive_spent_utxos(record); - cs.records.push((**record).clone()); - cs + CoreChangeSet { + new_utxos: derive_new_utxos(record), + spent_utxos: derive_spent_utxos(record), + records: vec![(**record).clone()], + ..CoreChangeSet::default() + } } WalletEvent::TransactionInstantLocked { wallet_id, diff --git a/packages/rs-platform-wallet/src/wallet/apply.rs b/packages/rs-platform-wallet/src/wallet/apply.rs index 57b23d7b39c..c73b7f4d162 100644 --- a/packages/rs-platform-wallet/src/wallet/apply.rs +++ b/packages/rs-platform-wallet/src/wallet/apply.rs @@ -313,7 +313,7 @@ impl PlatformWalletInfo { // `update_balance` returns its own changeset internally; we // discard it (apply does not re-emit). use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; - let _ = self.core_wallet.update_balance(); + self.core_wallet.update_balance(); // Mirror the recomputed balance into the lock-free Arc that the // UI reads. let core_balance = &self.core_wallet.balance; diff --git a/packages/rs-platform-wallet/tests/spv_sync.rs b/packages/rs-platform-wallet/tests/spv_sync.rs index a8c74183f5f..86011d5ea84 100644 --- a/packages/rs-platform-wallet/tests/spv_sync.rs +++ b/packages/rs-platform-wallet/tests/spv_sync.rs @@ -71,11 +71,7 @@ impl PlatformWalletPersistence for RecordingPersister { changeset: PlatformWalletChangeSet, ) -> Result<(), platform_wallet::changeset::PersistenceError> { let has_core = changeset.core.is_some(); - let synced_height = changeset - .core - .as_ref() - .and_then(|c| c.chain.as_ref()) - .and_then(|ch| ch.synced_height); + let synced_height = changeset.core.as_ref().and_then(|c| c.synced_height); self.records .lock() .unwrap() diff --git a/packages/wasm-dpp2/CONVENTIONS.md b/packages/wasm-dpp2/CONVENTIONS.md new file mode 100644 index 00000000000..35dd86b6f57 --- /dev/null +++ b/packages/wasm-dpp2/CONVENTIONS.md @@ -0,0 +1,326 @@ +# wasm-dpp2 Conventions + +This document captures the **single rule** for how protocol structs are exposed +to TypeScript/JavaScript through `wasm-dpp2`, and the small set of sub-rules +that follow from it. The goal is that every wrapper looks the same to a TS +developer — there are no per-type stylistic choices, no "this enum has `data` +because of a Rust mechanic," no manual reshaping in the WASM layer. + +## The principle + +> **`rs-dpp` serde defines the canonical wire shape. `wasm-dpp2` mirrors it +> 1:1, modulo primitive encoding.** + +If `serde_json::to_value(x)` in `rs-dpp` produces shape `S`, then +`xWasm.toJSON()` produces the same `S`, and `xWasm.toObject()` produces a +structurally identical shape with `Uint8Array` instead of base64 strings and +`bigint` instead of safe-number-or-string for large integers. + +Wrappers do not invent shapes. Wrappers do not reshape. Wrappers delegate. + +## wasm-dpp2 is a thin TypeScript convenience layer + +`wasm-dpp2` is not a validation tier. It does not duplicate, pre-empt, or +extend `rs-dpp` logic. Its job is to: + +- expose `rs-dpp` types to JavaScript through wasm-bindgen, +- adapt JS-friendly inputs (Options interfaces) into `rs-dpp` types, +- surface `rs-dpp`'s outputs (toObject / toJSON / getters) with TypeScript + types accurate to the actual wire shape. + +It is **not** the place to: + +- check field lengths, ranges, or counts that `rs-dpp` validates (e.g. + `encrypted_note.len() == 216`, `inputs.len() == input_witnesses.len()`, + `actions.len() <= max_actions`), +- enforce business rules, +- pre-validate state-transition structure. + +If `rs-dpp` enforces a constraint at structural validation, `wasm-dpp2` does +not duplicate it at the constructor boundary — even when the duplicate would +give an earlier or clearer error. Two reasons: + +1. **Single source of truth.** Constraints live in `rs-dpp` so they apply + uniformly across all consumers (Rust SDK, drive-abci, wasm-dpp2, …). A + second copy in `wasm-dpp2` drifts the moment `rs-dpp` changes its rule + and creates two definitions of "valid" with subtle disagreements. +2. **Layering.** Validation tiers (DPP structural validation, drive-abci + state validation, consensus) decide when to reject. The wasm boundary's + job is to construct the type, not gate-keep ahead of those tiers. + +The narrow exceptions (things that look like validation but aren't): + +- **Structural conversions required by the Rust type system** — e.g., + converting `Vec` to `[u8; N]` for fields that are already + fixed-size in `rs-dpp` (`anchor`, `bindingSignature`, etc.). The length + check is a side-effect of the type conversion, not a separate validation. +- **Information-preserving guards** — e.g., `inputs_to_btree_map` rejects + duplicate addresses because `BTreeMap` would silently drop the second + entry. The check protects against information loss during the conversion, + not against an `rs-dpp` constraint. +- **JS-side ergonomic conversions** — e.g., accepting a hex string or + `Uint8Array` for an address, parsing strings to numbers, etc. + +If you find yourself adding `if x.len() < N || x.len() > M` to a +constructor or setter, that's a `rs-dpp` validation function in disguise. +Move it. + +Wrappers do not invent shapes. Wrappers do not reshape. Wrappers delegate. + +## Sub-rules + +### Object vs JSON + +Both shapes are **structurally identical** — same fields, same nesting, same +discriminators. They differ **only in primitive encoding**: + +| | Object form (`toObject`) | JSON form (`toJSON`) | +| --- | --- | --- | +| Bytes (`[u8; N]`, `Vec`) | `Uint8Array` | base64 string | +| Identifiers | `Uint8Array` | base58 string | +| Addresses | `Uint8Array` | hex string | +| `u64` / `i64` (and aliases like `Credits`) | `bigint` | `number` if ≤ `MAX_SAFE_INTEGER`, else `string` | +| Everything else | passthrough | passthrough | + +The `#[json_safe_fields]` proc-macro injects the right `#[serde(with = ...)]` +helpers automatically for the integer and byte cases. + +### Tagged unions + +All sum types use **internal tagging**: `{ type: "variantName", ...fields }`. +No `data` wrapper. No external tagging. + +```typescript +// Good — internally tagged, flat. +type AssetLockProofObject = + | ({ type: "instant" } & InstantAssetLockProofObject) + | ({ type: "chain" } & ChainAssetLockProofObject); + +// Bad — adjacent tag (`data` wrapper). +type AssetLockProofObject = + | { type: "instant"; data: InstantAssetLockProofObject } // do NOT do this + | { type: "chain"; data: ChainAssetLockProofObject }; +``` + +In `rs-dpp`, this means `#[serde(tag = "type", rename_all = "camelCase")]` +without `content`. serde supports this on: + +- struct variants (`Variant { a: A, b: B }`) +- newtype variants whose inner type is a named struct + (`Variant(SomeNamedStruct)`) + +If a variant doesn't fit either shape (for example a tuple variant of a +non-struct type), the type needs a custom `Serialize` / `Deserialize` impl that +emits the flat shape manually. `AddressFundsFeeStrategyStep` and +`AddressWitness` are the existing precedents. + +### Versioning + +Versioned protocol structs use **`tag = "$formatVersion"`** with each variant +renamed to its version string: + +```rust +#[serde(tag = "$formatVersion")] +pub enum FooTransition { + #[serde(rename = "0")] + V0(FooTransitionV0), + #[serde(rename = "1")] + V1(FooTransitionV1), +} +``` + +`$formatVersion` is the universal key. Do **not** use `$version` for new +versioned enums — that key is reserved for legacy document/state-transition +protocol-version fields in `common_fields.rs`, and reusing it for versioned +serde tagging caused divergence in the past. + +### Wrapper plumbing + +A WASM wrapper is a transparent newtype over the inner DPP type: + +```rust +#[wasm_bindgen(js_name = "Foo")] +#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +pub struct FooWasm(Foo); +``` + +Conversions are wired up via one of two macros (defined in +`packages/wasm-dpp2/src/serialization/conversions.rs`): + +- **`impl_wasm_conversions_inner!`** — when the inner type implements + `JsonConvertible` and `ValueConvertible` (the trait-based path). Preferred. +- **`impl_wasm_conversions_serde!`** — when the inner type only has + `Serialize`/`Deserialize` (no trait impls). Used for newer types and shielded + wrappers. + +Both produce `toObject` / `toJSON` / `fromObject` / `fromJSON` with consistent +behavior. Direct `js_sys::Reflect::set` building of conversion shapes is +**forbidden**: the rs-dpp serde derive defines the shape and the macros +deliver it transparently. + +### Getters and setters + +The split: **field-like accessors are properties; verbs are methods.** + +In ideal JS conventions, properties are cheap and methods imply work. In +wasm-bindgen, **every** accessor crosses the wasm boundary — so there is no +truly "light" property anyway. The codebase reflects this: all field-like +accessors (including ones that clone Vecs, build maps, or wrap inner types) +are exposed as properties — `Identity.publicKeys`, `Document.properties`, +`IdentityCreditWithdrawalTransition.outputScript`, etc. Methods are reserved +for **actions**: things that take parameters, mutate beyond simple set, or +genuinely "do" something (`toBytes`, `toStateTransition`, `createIdentityId`). + +```rust +// Good — property style. JS sees `transition.amount`. +#[wasm_bindgen(getter = "amount")] +pub fn amount(&self) -> u64 { ... } + +#[wasm_bindgen(setter = "amount")] +pub fn set_amount(&mut self, value: u64) { ... } + +// Good — property even though it allocates a Vec (matches Identity.publicKeys). +#[wasm_bindgen(getter = "actions")] +pub fn actions(&self) -> Vec { ... } + +// Bad — method style for a field. JS sees `transition.getAmount()`. +#[wasm_bindgen(js_name = getAmount)] +pub fn get_amount(&self) -> u64 { ... } +``` + +The Rust function name should be the same as the field (no `get_` prefix), +and `getter = "..."` should carry the camelCase JS name. Setters use +`setter = "..."` with `set_` prefixed Rust fn names. + +**Return wasm wrapper types** when one exists for the field. Raw types +(`Vec`, primitives) are reserved for opaque crypto blobs (proofs, +signatures, anchors, encrypted notes) where no wrapper applies: + +```rust +// Good — typed wrappers for fields with wrappers. +#[wasm_bindgen(getter = "outputScript")] +pub fn output_script(&self) -> CoreScriptWasm { ... } + +#[wasm_bindgen(getter = "assetLockProof")] +pub fn asset_lock_proof(&self) -> AssetLockProofWasm { ... } + +// Good — raw bytes for opaque crypto blobs. +#[wasm_bindgen(getter = "proof")] +pub fn proof(&self) -> Vec { ... } // Halo2 proof bytes + +#[wasm_bindgen(getter = "anchor")] +pub fn anchor(&self) -> Vec { ... } // Sinsemilla root +``` + +For pooling enums and similar small enums that JS sees as named strings, +wire through the typed wasm enum so the conversion is centralized: + +```rust +#[wasm_bindgen(getter = "pooling")] +pub fn pooling(&self) -> String { + PoolingWasm::from(self.0.pooling()).into() +} +``` + +Don't expose `getType()` / `state_transition_type()` — that field isn't +exposed on any other transition wrapper, and JS callers can use +`instanceof` against the wasm class instead. + +### Constructor inputs + +`XxxOptions` constructor input types are **allowed to be looser** than the +output `Object` / `JSON` types: + +- optional fields with sensible defaults +- accept either a wasm wrapper instance or its plain-object form +- accept either `Uint8Array` or hex/base64 string for byte fields where + ergonomic + +The output shapes (`Object`, `JSON`) are strict and complete — no missing +fields, no per-call discretion. Constructors do the normalization. + +### Test coverage for `Object` / `JSON` round-trips + +Every wasm wrapper that exposes `toObject` / `fromObject` / `toJSON` / +`fromJSON` **must** have a unit test covering all four methods, with: + +1. **Per-property assertions on `toObject()` output.** Don't just assert the + call doesn't throw — assert the shape: instance types (`Uint8Array`, + `bigint`, `Map`), exact lengths for fixed-size byte fields, exact values + for known fixtures. + +2. **Per-property assertions on `toJSON()` output.** Same idea, JSON-side: + strings (base58 / base64 / hex), `string | number` for safe-integer u64s, + discriminator fields on tagged enums. + +3. **Round-trip via `fromObject(toObject())`.** Catches latent serde bugs in + the bytes / map deserialization path that pure `toObject()` shape checks + never exercise (this is how PR #3235 surfaced the + `serde_bytes_var` / `serde_bytes` `visit_bytes` gap). + +4. **Round-trip via `fromJSON(toJSON())`.** Same idea, JSON-side. Matters + especially for human-readable encodings of non-string types + (`bigint` ↔ string, `Identifier` ↔ base58, `BinaryData` ↔ base64). + +This applies to wasm-dpp2 wrappers and any wasm-sdk result type that +implements the same four methods (`PlatformAddressInfo`, +`ShieldedEncryptedNote`, `ShieldedNullifierStatus`, `ResponseMetadata`, +`ProofInfo`, `IdentityBalanceAndRevision`, etc.). + +> **Migration backlog** (see end of file): there are 78 wasm-dpp2 specs +> today; ~46 are missing `fromObject` coverage and ~37 are missing +> `fromJSON` coverage. Tracked separately from PR #3235. + +## Bincode is independent of serde + +Custom serde impls (used to reshape JSON output, like +`AddressFundsFeeStrategyStep`, `AssetLockProof`, `AddressWitness`, the +`address_funds/serde_helpers/` map reshapers) **do not touch bincode**. The +`Encode` / `Decode` derives drive the consensus binary format; serde drives +JSON / `platform_value`. Reshaping JSON for ergonomics is always safe as long +as bincode derives stay in place. + +## Migration backlog + +The following types still violate the convention and should be aligned in +follow-up PRs (separate from this one to keep blast radii sensible): + +- **Adjacent-tagged enums still emitting `{type, data}`** (8 in rs-dpp), to be + flattened to internal tagging: + - `Vote` (`packages/rs-dpp/src/voting/votes/mod.rs`) + - `VotePoll` (`packages/rs-dpp/src/voting/vote_polls/mod.rs`) + - `ResourceVoteChoice` + (`packages/rs-dpp/src/voting/vote_choices/resource_vote_choice/mod.rs`) + - `TokenEvent` (`packages/rs-dpp/src/tokens/token_event.rs`) + - `GroupActionEvent` (`packages/rs-dpp/src/group/action_event.rs`) + - The two `AssetLockProof` derives in + `packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs` + are already flat as of PR #3235. + - The associated wasm-dpp2 wrappers (`ResourceVoteChoiceObject`, + `ContestedDocumentVotePollWinnerInfoObject`) drop their `data` slots once + the rs-dpp side flattens. + + Note: serde's internal tagging works on **struct variants** and on **newtype + variants whose inner type is a named struct**. If a tuple variant wraps a + non-struct type (a `Uint8Array`, a primitive, a tuple), the enum needs a + custom `Serialize`/`Deserialize` impl that flattens manually — same pattern + `AddressFundsFeeStrategyStep` and `AddressWitness` already use. + +- **Parent state transitions have no `toObject` / `toJSON`** — only + `toBytes`/`toHex`/`toBase64`. Sub-transitions do, but the wrapping + `StateTransition` does not. Either add the methods following this + convention, or document the deliberate gap in this file. + +- **`tag = "$version"` → `tag = "$formatVersion"` migration** — fixed for the + 5 shielded transitions in PR #3235. Audit anything else still on `$version` + in newer code. + +- **Round-trip + per-property test coverage** — see "Test coverage for + `Object` / `JSON` round-trips" above. As of PR #3235, ~46 of the 78 + wasm-dpp2 specs are missing `fromObject` coverage and ~37 are missing + `fromJSON` coverage. Same pattern needed in wasm-sdk for its result + wrappers. Track as a dedicated cleanup PR — large mechanical sweep, + expected to surface latent serde bugs along the way (the + `serde_bytes_var` / `serde_bytes` `visit_bytes` gap is the first known + example, fixed in PR #3235). diff --git a/packages/wasm-dpp2/src/asset_lock_proof/outpoint.rs b/packages/wasm-dpp2/src/asset_lock_proof/outpoint.rs index 017f5ead49f..ac9b509ef1a 100644 --- a/packages/wasm-dpp2/src/asset_lock_proof/outpoint.rs +++ b/packages/wasm-dpp2/src/asset_lock_proof/outpoint.rs @@ -1,6 +1,6 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_wasm_type_info; -use crate::utils::IntoWasm; +use crate::utils::{IntoWasm, try_vec_to_fixed_bytes}; use dpp::dashcore::{OutPoint, Txid}; use dpp::platform_value::string_encoding::Encoding::{Base64, Hex}; use dpp::platform_value::string_encoding::{decode, encode}; @@ -117,14 +117,7 @@ impl OutPointWasm { #[wasm_bindgen(js_name = "fromBytes")] pub fn from_bytes(buffer: Vec) -> WasmDppResult { - if buffer.len() != 36 { - return Err(WasmDppError::invalid_argument(format!( - "OutPoint must be exactly 36 bytes, got {}", - buffer.len() - ))); - } - - let out_buffer: [u8; 36] = buffer.try_into().expect("length already validated"); + let out_buffer: [u8; 36] = try_vec_to_fixed_bytes(buffer, "outPoint")?; Ok(OutPointWasm(OutPoint::from(out_buffer))) } diff --git a/packages/wasm-dpp2/src/asset_lock_proof/proof.rs b/packages/wasm-dpp2/src/asset_lock_proof/proof.rs index a078b1f21d8..451c3765b6a 100644 --- a/packages/wasm-dpp2/src/asset_lock_proof/proof.rs +++ b/packages/wasm-dpp2/src/asset_lock_proof/proof.rs @@ -11,28 +11,30 @@ use crate::identifier::IdentifierWasm; use crate::impl_try_from_js_value; use crate::impl_try_from_options; use crate::impl_wasm_type_info; -use crate::utils::{IntoWasm, get_class_type, try_from_options}; +use crate::utils::{IntoWasm, get_class_type}; use dpp::prelude::AssetLockProof; -use js_sys::Reflect; use wasm_bindgen::prelude::*; #[wasm_bindgen(typescript_custom_section)] const TS_TYPES: &str = r#" /** * AssetLockProof serialized as a plain object. - * Type 0 = Instant, Type 1 = Chain. + * + * Internally-tagged discriminated union — `type` discriminates the variant and + * the variant's fields sit alongside it. Mirrors the rs-dpp serde shape (which + * uses `#[serde(tag = "type")]` on the enum) and the convention used by + * `AddressWitness` / `AddressFundsFeeStrategyStep`. */ export type AssetLockProofObject = - | ({ type: 0 } & InstantAssetLockProofObject) - | ({ type: 1 } & ChainAssetLockProofObject); + | ({ type: "instant" } & InstantAssetLockProofObject) + | ({ type: "chain" } & ChainAssetLockProofObject); /** * AssetLockProof serialized as JSON. - * Type 0 = Instant, Type 1 = Chain. */ export type AssetLockProofJSON = - | ({ type: 0 } & InstantAssetLockProofJSON) - | ({ type: 1 } & ChainAssetLockProofJSON); + | ({ type: "instant" } & InstantAssetLockProofJSON) + | ({ type: "chain" } & ChainAssetLockProofJSON); "#; #[wasm_bindgen] @@ -69,7 +71,8 @@ impl From for ChainAssetLockProofJSONJs { } #[wasm_bindgen(js_name = "AssetLockProof")] -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] pub struct AssetLockProofWasm(AssetLockProof); impl From for AssetLockProof { @@ -162,19 +165,14 @@ impl AssetLockProofWasm { Ok(ChainAssetLockProofWasm::constructor(core_chain_locked_height, out_point)?.into()) } + /// Returns the lock type as a lowercase wire-shape string ("instant" or + /// "chain") — matching the `type` discriminator emitted by `toObject()` / + /// `toJSON()`. #[wasm_bindgen(getter = "lockType")] - pub fn lock_type(&self) -> AssetLockProofTypeWasm { + pub fn lock_type(&self) -> String { match self.0 { - AssetLockProof::Chain(_) => AssetLockProofTypeWasm::Chain, - AssetLockProof::Instant(_) => AssetLockProofTypeWasm::Instant, - } - } - - #[wasm_bindgen(getter = "lockTypeName")] - pub fn lock_type_name(&self) -> String { - match self.0 { - AssetLockProof::Chain(_) => AssetLockProofTypeWasm::Chain.into(), AssetLockProof::Instant(_) => AssetLockProofTypeWasm::Instant.into(), + AssetLockProof::Chain(_) => AssetLockProofTypeWasm::Chain.into(), } } @@ -200,86 +198,6 @@ impl AssetLockProofWasm { Ok(identifier.into()) } - #[wasm_bindgen(js_name = "toObject")] - pub fn to_object(&self) -> WasmDppResult { - let inner_object: JsValue = match &self.0 { - AssetLockProof::Chain(chain) => ChainAssetLockProofWasm::from(chain.clone()) - .to_object()? - .into(), - AssetLockProof::Instant(instant) => InstantAssetLockProofWasm::from(instant.clone()) - .to_object()? - .into(), - }; - - // Add type field: 0 = Instant, 1 = Chain - let proof_type: u8 = match &self.0 { - AssetLockProof::Instant(_) => 0, - AssetLockProof::Chain(_) => 1, - }; - Reflect::set( - &inner_object, - &JsValue::from_str("type"), - &JsValue::from(proof_type), - ) - .map_err(|e| WasmDppError::serialization(format!("{:?}", e)))?; - - Ok(inner_object.into()) - } - - #[wasm_bindgen(js_name = "fromObject")] - pub fn from_object(object: AssetLockProofObjectJs) -> WasmDppResult { - let proof_type: AssetLockProofTypeWasm = try_from_options(&object, "type")?; - - match proof_type { - AssetLockProofTypeWasm::Instant => { - InstantAssetLockProofWasm::from_object(object.into()).map(AssetLockProofWasm::from) - } - AssetLockProofTypeWasm::Chain => { - ChainAssetLockProofWasm::from_object(object.into()).map(AssetLockProofWasm::from) - } - } - } - - #[wasm_bindgen(js_name = "toJSON")] - pub fn to_json(&self) -> WasmDppResult { - let inner_json: JsValue = match &self.0 { - AssetLockProof::Chain(chain) => ChainAssetLockProofWasm::from(chain.clone()) - .to_json()? - .into(), - AssetLockProof::Instant(instant) => InstantAssetLockProofWasm::from(instant.clone()) - .to_json()? - .into(), - }; - - // Add type field: 0 = Instant, 1 = Chain - let proof_type: u8 = match &self.0 { - AssetLockProof::Instant(_) => 0, - AssetLockProof::Chain(_) => 1, - }; - Reflect::set( - &inner_json, - &JsValue::from_str("type"), - &JsValue::from(proof_type), - ) - .map_err(|e| WasmDppError::serialization(format!("{:?}", e)))?; - - Ok(inner_json.into()) - } - - #[wasm_bindgen(js_name = "fromJSON")] - pub fn from_json(object: AssetLockProofJSONJs) -> WasmDppResult { - let proof_type: AssetLockProofTypeWasm = try_from_options(&object, "type")?; - - match proof_type { - AssetLockProofTypeWasm::Instant => { - InstantAssetLockProofWasm::from_json(object.into()).map(AssetLockProofWasm::from) - } - AssetLockProofTypeWasm::Chain => { - ChainAssetLockProofWasm::from_json(object.into()).map(AssetLockProofWasm::from) - } - } - } - #[wasm_bindgen(js_name = "toHex")] pub fn to_hex(&self) -> WasmDppResult { let bytes = bincode::encode_to_vec(&self.0, bincode::config::standard()) @@ -317,3 +235,9 @@ impl AssetLockProofWasm { impl_try_from_js_value!(AssetLockProofWasm, "AssetLockProof"); impl_try_from_options!(AssetLockProofWasm); impl_wasm_type_info!(AssetLockProofWasm, AssetLockProof); +crate::impl_wasm_conversions_serde!( + AssetLockProofWasm, + AssetLockProof, + AssetLockProofObjectJs, + AssetLockProofJSONJs +); diff --git a/packages/wasm-dpp2/src/core/core_script.rs b/packages/wasm-dpp2/src/core/core_script.rs index 64735b05800..0680cca16d7 100644 --- a/packages/wasm-dpp2/src/core/core_script.rs +++ b/packages/wasm-dpp2/src/core/core_script.rs @@ -3,6 +3,7 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_try_from_js_value; use crate::impl_try_from_options; use crate::impl_wasm_type_info; +use crate::utils::try_vec_to_fixed_bytes; use dpp::dashcore::address::Payload; use dpp::dashcore::{Address, opcodes}; use dpp::identity::core_script::CoreScript; @@ -37,14 +38,7 @@ impl CoreScriptWasm { pub fn from_p2pkh( #[wasm_bindgen(js_name = "keyHash")] key_hash: Vec, ) -> WasmDppResult { - if key_hash.len() != 20 { - return Err(WasmDppError::invalid_argument(format!( - "P2PKH key hash must be exactly 20 bytes, got {}", - key_hash.len() - ))); - } - - let key_hash_bytes: [u8; 20] = key_hash.try_into().expect("length already validated"); + let key_hash_bytes: [u8; 20] = try_vec_to_fixed_bytes(key_hash, "keyHash")?; Ok(CoreScriptWasm(CoreScript::new_p2pkh(key_hash_bytes))) } @@ -53,14 +47,7 @@ impl CoreScriptWasm { pub fn from_p2sh( #[wasm_bindgen(js_name = "scriptHash")] script_hash: Vec, ) -> WasmDppResult { - if script_hash.len() != 20 { - return Err(WasmDppError::invalid_argument(format!( - "P2SH script hash must be exactly 20 bytes, got {}", - script_hash.len() - ))); - } - - let script_hash_bytes: [u8; 20] = script_hash.try_into().expect("length already validated"); + let script_hash_bytes: [u8; 20] = try_vec_to_fixed_bytes(script_hash, "scriptHash")?; let mut bytes = vec![ opcodes::all::OP_HASH160.to_u8(), diff --git a/packages/wasm-dpp2/src/core/private_key.rs b/packages/wasm-dpp2/src/core/private_key.rs index f39c8f3c950..17a41c969e8 100644 --- a/packages/wasm-dpp2/src/core/private_key.rs +++ b/packages/wasm-dpp2/src/core/private_key.rs @@ -6,6 +6,7 @@ use crate::impl_try_from_js_value; use crate::impl_try_from_options; use crate::impl_wasm_type_info; use crate::public_key::PublicKeyWasm; +use crate::utils::try_vec_to_fixed_bytes; use dpp::dashcore::PrivateKey; use dpp::dashcore::hashes::hex::FromHex; use dpp::dashcore::key::Secp256k1; @@ -49,9 +50,7 @@ impl PrivateKeyWasm { pub fn from_bytes(bytes: Vec, network: NetworkLikeJs) -> WasmDppResult { let network_wasm: NetworkWasm = network.try_into()?; - let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| { - WasmDppError::invalid_argument("Private key bytes must be exactly 32 bytes".to_string()) - })?; + let key_bytes: [u8; 32] = try_vec_to_fixed_bytes(bytes, "privateKey")?; let pk = PrivateKey::from_byte_array(&key_bytes, network_wasm.into()) .map_err(|err| WasmDppError::invalid_argument(err.to_string()))?; @@ -69,9 +68,7 @@ impl PrivateKeyWasm { let bytes = Vec::from_hex(hex_key) .map_err(|err| WasmDppError::invalid_argument(err.to_string()))?; - let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| { - WasmDppError::invalid_argument("Private key hex must decode to 32 bytes".to_string()) - })?; + let key_bytes: [u8; 32] = try_vec_to_fixed_bytes(bytes, "privateKey")?; let pk = PrivateKey::from_byte_array(&key_bytes, network_wasm.into()) .map_err(|err| WasmDppError::invalid_argument(err.to_string()))?; diff --git a/packages/wasm-dpp2/src/data_contract/document/model.rs b/packages/wasm-dpp2/src/data_contract/document/model.rs index d9c18905073..27cf5006d48 100644 --- a/packages/wasm-dpp2/src/data_contract/document/model.rs +++ b/packages/wasm-dpp2/src/data_contract/document/model.rs @@ -7,6 +7,7 @@ use crate::impl_wasm_type_info; use crate::serialization; use crate::utils::{ ToSerdeJSONExt, try_from_options, try_from_options_optional, try_from_options_with, + try_vec_to_fixed_bytes, }; use crate::version::{PlatformVersionLikeJs, PlatformVersionWasm}; use dpp::document::serialization_traits::{ @@ -355,15 +356,7 @@ impl DocumentWasm { self.entropy = None; } Some(bytes) => { - if bytes.len() != 32 { - return Err(WasmDppError::invalid_argument(format!( - "Entropy must be exactly 32 bytes, got {}", - bytes.len() - ))); - } - let mut entropy_bytes = [0u8; 32]; - entropy_bytes.copy_from_slice(&bytes); - self.entropy = Some(entropy_bytes); + self.entropy = Some(try_vec_to_fixed_bytes(bytes, "entropy")?); } } Ok(()) @@ -708,17 +701,7 @@ impl DocumentWasm { let data_contract_id: Identifier = data_contract_id.try_into()?; let entropy_bytes: [u8; 32] = match entropy { - Some(entropy_vec) => { - if entropy_vec.len() != 32 { - return Err(WasmDppError::invalid_argument(format!( - "Entropy must be exactly 32 bytes, got {}", - entropy_vec.len() - ))); - } - let mut arr = [0u8; 32]; - arr.copy_from_slice(&entropy_vec); - arr - } + Some(entropy_vec) => try_vec_to_fixed_bytes(entropy_vec, "entropy")?, None => entropy_generator::DefaultEntropyGenerator .generate() .map_err(|err| WasmDppError::serialization(err.to_string()))?, diff --git a/packages/wasm-dpp2/src/enums/lock_types.rs b/packages/wasm-dpp2/src/enums/lock_types.rs index cc240993903..fd887074935 100644 --- a/packages/wasm-dpp2/src/enums/lock_types.rs +++ b/packages/wasm-dpp2/src/enums/lock_types.rs @@ -1,46 +1,47 @@ +//! Internal Rust-side discriminator for `AssetLockProof` variants. +//! +//! Not exposed to JS as a wasm-bindgen enum (numeric enums are unidiomatic at +//! the JS / TS boundary; the wire shape uses lowercase strings "instant" / +//! "chain", matching `AssetLockProof::toObject()` / `toJSON()`). JS-facing +//! getters return the lowercase string directly via the `Display` impl below. + use crate::error::WasmDppError; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::{JsError, JsValue}; +use wasm_bindgen::JsValue; -#[wasm_bindgen(js_name = "AssetLockProofType")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum AssetLockProofTypeWasm { - Instant = 0, - Chain = 1, + Instant, + Chain, +} + +impl AssetLockProofTypeWasm { + /// Lowercase wire-shape name ("instant" / "chain"), matching the + /// adjacent-tagged `{ type, data }` shape emitted by `AssetLockProof.toObject()`. + pub fn as_wire_name(self) -> &'static str { + match self { + AssetLockProofTypeWasm::Instant => "instant", + AssetLockProofTypeWasm::Chain => "chain", + } + } } impl TryFrom<&JsValue> for AssetLockProofTypeWasm { type Error = WasmDppError; fn try_from(value: &JsValue) -> Result { - if value.is_string() { - match value.as_string() { - None => Err(WasmDppError::invalid_argument( - "cannot read value from enum", - )), - Some(enum_val) => match enum_val.to_lowercase().as_str() { - "instant" => Ok(AssetLockProofTypeWasm::Instant), - "chain" => Ok(AssetLockProofTypeWasm::Chain), - _ => Err(WasmDppError::invalid_argument(format!( - "unsupported lock type {}", - enum_val - ))), - }, - } - } else { - match value.as_f64() { - None => Err(WasmDppError::invalid_argument( - "cannot read value from enum", - )), - Some(enum_val) => match enum_val as u8 { - 0 => Ok(AssetLockProofTypeWasm::Instant), - 1 => Ok(AssetLockProofTypeWasm::Chain), - _ => Err(WasmDppError::invalid_argument(format!( - "unsupported lock type {}", - enum_val - ))), - }, - } + if let Some(s) = value.as_string() { + return match s.to_lowercase().as_str() { + "instant" => Ok(AssetLockProofTypeWasm::Instant), + "chain" => Ok(AssetLockProofTypeWasm::Chain), + other => Err(WasmDppError::invalid_argument(format!( + "unsupported lock type '{}', expected \"instant\" or \"chain\"", + other + ))), + }; } + Err(WasmDppError::invalid_argument( + "AssetLockProof type must be a string (\"instant\" or \"chain\")", + )) } } @@ -54,32 +55,6 @@ impl TryFrom for AssetLockProofTypeWasm { impl From for String { fn from(value: AssetLockProofTypeWasm) -> Self { - match value { - AssetLockProofTypeWasm::Instant => String::from("Instant"), - AssetLockProofTypeWasm::Chain => String::from("Chain"), - } - } -} - -impl TryFrom for AssetLockProofTypeWasm { - type Error = JsError; - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(Self::Instant), - 1 => Ok(Self::Chain), - _ => Err(JsError::new("Unexpected asset lock proof type")), - } - } -} - -impl TryFrom for AssetLockProofTypeWasm { - type Error = JsError; - - fn try_from(value: u64) -> Result { - match value { - 0 => Ok(Self::Instant), - 1 => Ok(Self::Chain), - _ => Err(JsError::new("Unexpected asset lock proof type")), - } + value.as_wire_name().to_string() } } diff --git a/packages/wasm-dpp2/src/identity/public_key.rs b/packages/wasm-dpp2/src/identity/public_key.rs index 099b42c47ee..0486d26d569 100644 --- a/packages/wasm-dpp2/src/identity/public_key.rs +++ b/packages/wasm-dpp2/src/identity/public_key.rs @@ -10,6 +10,7 @@ use crate::impl_wasm_type_info; use crate::serialization; use crate::utils::{ try_from_options, try_from_options_optional, try_to_fixed_bytes, try_to_u32, try_to_u64, + try_vec_to_fixed_bytes, }; use crate::version::PlatformVersionLikeJs; use dpp::dashcore::Network; @@ -86,7 +87,7 @@ export interface IdentityPublicKeyJSON { id: number; purpose: number; securityLevel: number; - contractBounds?: object; + contractBounds?: ContractBoundsJSON; type: number; readOnly: boolean; data: string; @@ -198,14 +199,8 @@ impl IdentityPublicKeyWasm { private_key_bytes_input: Vec, network: NetworkLikeJs, ) -> WasmDppResult { - if private_key_bytes_input.len() != 32 { - return Err(WasmDppError::invalid_argument(format!( - "Private key must be exactly 32 bytes, got {}", - private_key_bytes_input.len() - ))); - } - let mut private_key_bytes = [0u8; 32]; - private_key_bytes.copy_from_slice(&private_key_bytes_input); + let private_key_bytes: [u8; 32] = + try_vec_to_fixed_bytes(private_key_bytes_input, "privateKey")?; let network: Network = network.try_into()?; diff --git a/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs b/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs index a8695b2253b..6ec19b2a9d5 100644 --- a/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs +++ b/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs @@ -29,7 +29,7 @@ use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen(typescript_custom_section)] const CREDIT_WITHDRAWAL_OPTIONS_TS: &str = r#" -export type CreditWithdrawalTransitionPoolingLike = CreditWithdrawalTransitionPooling | string | number; +export type CreditWithdrawalTransitionPoolingLike = PoolingWasm | string | number; export interface IdentityCreditWithdrawalTransitionOptions { identityId: IdentifierLike; @@ -48,7 +48,7 @@ export interface IdentityCreditWithdrawalTransitionObject { identityId: Uint8Array; amount: bigint; coreFeePerByte: number; - pooling: CreditWithdrawalTransitionPooling; + pooling: PoolingWasm; outputScript?: Uint8Array; nonce: bigint; userFeeIncrease: number; diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index 8ff7050bff0..d687c7c5445 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; @@ -64,7 +65,13 @@ pub use platform_address::{ FeeStrategyStepWasm, PlatformAddressInputWasm, PlatformAddressLikeArrayJs, PlatformAddressLikeJs, PlatformAddressOutputWasm, PlatformAddressSignerWasm, PlatformAddressWasm, default_fee_strategy, fee_strategy_from_steps, - fee_strategy_from_steps_or_default, outputs_to_btree_map, outputs_to_optional_btree_map, + fee_strategy_from_steps_or_default, inputs_to_btree_map, outputs_to_btree_map, + outputs_to_optional_btree_map, +}; +pub use shielded::{ + AddressWitnessWasm, SerializedOrchardActionWasm, ShieldFromAssetLockTransitionWasm, + ShieldTransitionWasm, ShieldedTransferTransitionWasm, ShieldedWithdrawalTransitionWasm, + UnshieldTransitionWasm, }; pub use state_transitions::base::{GroupStateTransitionInfoWasm, StateTransitionWasm}; pub use state_transitions::proof_result::{StateTransitionProofResultTypeJs, convert_proof_result}; diff --git a/packages/wasm-dpp2/src/platform_address/fee_strategy.rs b/packages/wasm-dpp2/src/platform_address/fee_strategy.rs index 39b2878d2a1..938f1e2e6cb 100644 --- a/packages/wasm-dpp2/src/platform_address/fee_strategy.rs +++ b/packages/wasm-dpp2/src/platform_address/fee_strategy.rs @@ -2,17 +2,45 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_wasm_type_info; use crate::utils::{IntoWasm, try_from_options_optional_with, try_to_array}; use dpp::address_funds::{AddressFundsFeeStrategy, AddressFundsFeeStrategyStep}; -use serde::Deserialize; -use serde::de::{self, Deserializer, MapAccess, Visitor}; -use std::fmt; +use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; +#[wasm_bindgen(typescript_custom_section)] +const FEE_STRATEGY_STEP_TS_TYPES: &str = r#" +/** + * Fee strategy step in Object form (output of a transition's `toObject()`). + * + * Discriminated by `type`: "deductFromInput" reduces an input's contribution + * by the fee, "reduceOutput" reduces an output's amount by the fee. The + * `index` selects the input/output position. + */ +export type FeeStrategyStepObject = + | { type: "deductFromInput"; index: number } + | { type: "reduceOutput"; index: number }; + +/** + * Fee strategy step in JSON form (output of a transition's `toJSON()`). + * + * Identical shape to `FeeStrategyStepObject` because the only payload is a + * small `index` (u16) which serializes the same way in both binary and + * human-readable formats. + */ +export type FeeStrategyStepJSON = + | { type: "deductFromInput"; index: number } + | { type: "reduceOutput"; index: number }; +"#; + /// Defines how fees are paid in address-based state transitions. /// /// Fee strategy is a sequence of steps that determine which inputs or outputs /// should be reduced to cover the transaction fee. +/// +/// `#[serde(transparent)]` delegates to the inner `AddressFundsFeeStrategyStep`'s +/// custom serde, which produces the `{ type, index }` adjacent shape used by +/// every wasm-sdk consumer that round-trips a `Vec`. #[wasm_bindgen(js_name = "FeeStrategyStep")] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(transparent)] pub struct FeeStrategyStepWasm(AddressFundsFeeStrategyStep); #[wasm_bindgen(js_class = FeeStrategyStep)] @@ -117,67 +145,3 @@ pub fn fee_strategy_from_js_options( .collect() }) } - -impl<'de> Deserialize<'de> for FeeStrategyStepWasm { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "camelCase")] - enum Field { - Type, - Index, - } - - struct FeeStrategyStepVisitor; - - impl<'de> Visitor<'de> for FeeStrategyStepVisitor { - type Value = FeeStrategyStepWasm; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("struct FeeStrategyStep with type and index") - } - - fn visit_map(self, mut map: V) -> Result - where - V: MapAccess<'de>, - { - let mut step_type: Option = None; - let mut index: Option = None; - - while let Some(key) = map.next_key()? { - match key { - Field::Type => { - if step_type.is_some() { - return Err(de::Error::duplicate_field("type")); - } - step_type = Some(map.next_value()?); - } - Field::Index => { - if index.is_some() { - return Err(de::Error::duplicate_field("index")); - } - index = Some(map.next_value()?); - } - } - } - - let step_type = step_type.ok_or_else(|| de::Error::missing_field("type"))?; - let index = index.ok_or_else(|| de::Error::missing_field("index"))?; - - match step_type.as_str() { - "deductFromInput" => Ok(FeeStrategyStepWasm::deduct_from_input(index)), - "reduceOutput" => Ok(FeeStrategyStepWasm::reduce_output(index)), - _ => Err(de::Error::unknown_variant( - &step_type, - &["deductFromInput", "reduceOutput"], - )), - } - } - } - - const FIELDS: &[&str] = &["type", "index"]; - deserializer.deserialize_struct("FeeStrategyStep", FIELDS, FeeStrategyStepVisitor) - } -} diff --git a/packages/wasm-dpp2/src/platform_address/input_output.rs b/packages/wasm-dpp2/src/platform_address/input_output.rs index 635bca6f5a7..70ff7bd8080 100644 --- a/packages/wasm-dpp2/src/platform_address/input_output.rs +++ b/packages/wasm-dpp2/src/platform_address/input_output.rs @@ -11,6 +11,52 @@ use serde::Deserialize; use std::collections::BTreeMap; use wasm_bindgen::prelude::*; +#[wasm_bindgen(typescript_custom_section)] +const PLATFORM_ADDRESS_INPUT_OUTPUT_TS_TYPES: &str = r#" +/** + * Input address spending credits — Object form (output of a transition's `toObject()`). + * + * `address` is the 21-byte PlatformAddress (1 type byte + 20-byte hash) as a Uint8Array. + */ +export interface PlatformAddressInputObject { + address: Uint8Array; + nonce: number; + amount: bigint; +} + +/** + * Input address spending credits — JSON form (output of a transition's `toJSON()`). + * + * `address` is hex-encoded; `amount` may be a string when above `Number.MAX_SAFE_INTEGER`. + */ +export interface PlatformAddressInputJSON { + address: string; + nonce: number; + amount: number | string; +} + +/** + * Output address receiving credits — Object form. + * + * `amount` is `null` only for asset-lock funding transitions, where exactly one + * output (acting as the change recipient) absorbs the asset-lock remainder. For + * all other transitions (transfer / withdrawal / identity flows / credit transfer) + * the amount is always present. + */ +export interface PlatformAddressOutputObject { + address: Uint8Array; + amount: bigint | null; +} + +/** + * Output address receiving credits — JSON form. + */ +export interface PlatformAddressOutputJSON { + address: string; + amount: number | string | null; +} +"#; + /// Represents an input address for address-based state transitions. /// /// An input specifies a Platform address that will spend credits, @@ -165,25 +211,65 @@ impl PlatformAddressOutputWasm { } } +/// Converts a vector of `PlatformAddressInputWasm` into a `BTreeMap`, +/// returning an error if any address appears more than once. +/// +/// `BTreeMap::collect()` on its own would silently overwrite duplicates, +/// which is dangerous: a JS caller could pass two entries for the same +/// address with different amounts and only the last one would survive. +pub fn inputs_to_btree_map( + inputs: Vec, +) -> WasmDppResult> { + let mut map = BTreeMap::new(); + for input in inputs { + let (address, value) = input.into_inner(); + if map.insert(address, value).is_some() { + return Err(WasmDppError::invalid_argument(format!( + "duplicate input address: {}", + hex::encode(address.to_bytes()) + ))); + } + } + Ok(map) +} + /// Converts a vector of PlatformAddressOutput into a BTreeMap. -/// Returns an error if any output has no amount set. +/// Returns an error if any output has no amount set or if an address is duplicated. pub fn outputs_to_btree_map( outputs: Vec, ) -> WasmDppResult> { - outputs.into_iter().map(|o| o.try_into_inner()).collect() + let mut map = BTreeMap::new(); + for output in outputs { + let (address, amount) = output.try_into_inner()?; + if map.insert(address, amount).is_some() { + return Err(WasmDppError::invalid_argument(format!( + "duplicate output address: {}", + hex::encode(address.to_bytes()) + ))); + } + } + Ok(map) } /// Converts a vector of PlatformAddressOutput into a BTreeMap with optional amounts. /// /// Used for asset lock funding where the amount is optional (None means -/// the system distributes the asset lock funds automatically). +/// the system distributes the asset lock funds automatically). Returns an +/// error if an address is duplicated. pub fn outputs_to_optional_btree_map( outputs: Vec, -) -> BTreeMap> { - outputs - .into_iter() - .map(|o| o.into_inner_optional()) - .collect() +) -> WasmDppResult>> { + let mut map = BTreeMap::new(); + for output in outputs { + let (address, amount) = output.into_inner_optional(); + if map.insert(address, amount).is_some() { + return Err(WasmDppError::invalid_argument(format!( + "duplicate output address: {}", + hex::encode(address.to_bytes()) + ))); + } + } + Ok(map) } /// Extract a Vec from a JS options object property. diff --git a/packages/wasm-dpp2/src/platform_address/mod.rs b/packages/wasm-dpp2/src/platform_address/mod.rs index 3808b2a15ef..db0da8d5b4f 100644 --- a/packages/wasm-dpp2/src/platform_address/mod.rs +++ b/packages/wasm-dpp2/src/platform_address/mod.rs @@ -11,6 +11,7 @@ pub use fee_strategy::{ }; pub use input_output::{ PlatformAddressInputWasm, PlatformAddressOutputWasm, inputs_from_js_options, - outputs_from_js_options, outputs_to_btree_map, outputs_to_optional_btree_map, + inputs_to_btree_map, outputs_from_js_options, outputs_to_btree_map, + outputs_to_optional_btree_map, }; pub use signer::PlatformAddressSignerWasm; diff --git a/packages/wasm-dpp2/src/platform_address/transitions/address_credit_withdrawal_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/address_credit_withdrawal_transition.rs index d86f3da668d..a59a8bdaf25 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/address_credit_withdrawal_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/address_credit_withdrawal_transition.rs @@ -45,12 +45,12 @@ export interface AddressCreditWithdrawalTransitionObject { } export interface AddressCreditWithdrawalTransitionJSON { - inputs: object[]; - output?: object; + inputs: PlatformAddressInputJSON[]; + output?: PlatformAddressOutputJSON; outputScript: string; pooling: string; coreFeePerByte: number; - feeStrategy: object[]; + feeStrategy: FeeStrategyStepJSON[]; userFeeIncrease: number; } "#; @@ -98,7 +98,7 @@ impl AddressCreditWithdrawalTransitionWasm { })? .unwrap_or(0); - let inputs_map = inputs.into_iter().map(|i| i.into_inner()).collect(); + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; let output = output.map(|o| o.try_into_inner()).transpose()?; let fee_strategy = fee_strategy_from_steps_or_default(fee_strategy); @@ -168,13 +168,14 @@ impl AddressCreditWithdrawalTransitionWasm { } #[wasm_bindgen(setter = "inputs")] - pub fn set_inputs(&mut self, inputs: Vec) { - let inputs_map = inputs.into_iter().map(|i| i.into_inner()).collect(); + pub fn set_inputs(&mut self, inputs: Vec) -> WasmDppResult<()> { + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; match &mut self.0 { AddressCreditWithdrawalTransition::V0(v0) => { v0.inputs = inputs_map; } } + Ok(()) } #[wasm_bindgen(getter = "output")] diff --git a/packages/wasm-dpp2/src/platform_address/transitions/address_funding_from_asset_lock_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/address_funding_from_asset_lock_transition.rs index 0415ba6d967..4236aa278bf 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/address_funding_from_asset_lock_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/address_funding_from_asset_lock_transition.rs @@ -39,9 +39,9 @@ export interface AddressFundingFromAssetLockTransitionObject { export interface AddressFundingFromAssetLockTransitionJSON { assetLockProof: AssetLockProofJSON; - inputs: object[]; - outputs: object[]; - feeStrategy: object[]; + inputs: PlatformAddressInputJSON[]; + outputs: PlatformAddressOutputJSON[]; + feeStrategy: FeeStrategyStepJSON[]; userFeeIncrease: number; } "#; @@ -84,11 +84,8 @@ impl AddressFundingFromAssetLockTransitionWasm { })? .unwrap_or(0); - let inputs_map = inputs.into_iter().map(|i| i.into_inner()).collect(); - let outputs = outputs - .into_iter() - .map(|o| o.into_inner_optional()) - .collect(); + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; + let outputs = crate::platform_address::outputs_to_optional_btree_map(outputs)?; let fee_strategy = fee_strategy_from_steps_or_default(fee_strategy); Ok(AddressFundingFromAssetLockTransitionWasm( @@ -166,13 +163,14 @@ impl AddressFundingFromAssetLockTransitionWasm { } #[wasm_bindgen(setter = "inputs")] - pub fn set_inputs(&mut self, inputs: Vec) { - let inputs_map = inputs.into_iter().map(|i| i.into_inner()).collect(); + pub fn set_inputs(&mut self, inputs: Vec) -> WasmDppResult<()> { + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; match &mut self.0 { AddressFundingFromAssetLockTransition::V0(v0) => { v0.inputs = inputs_map; } } + Ok(()) } #[wasm_bindgen(getter = "outputs")] @@ -185,12 +183,10 @@ impl AddressFundingFromAssetLockTransitionWasm { } #[wasm_bindgen(setter = "outputs")] - pub fn set_outputs(&mut self, outputs: Vec) { - let outputs_map = outputs - .into_iter() - .map(|o| o.into_inner_optional()) - .collect(); + pub fn set_outputs(&mut self, outputs: Vec) -> WasmDppResult<()> { + let outputs_map = crate::platform_address::outputs_to_optional_btree_map(outputs)?; self.0.set_outputs(outputs_map); + Ok(()) } #[wasm_bindgen(getter = "userFeeIncrease")] diff --git a/packages/wasm-dpp2/src/platform_address/transitions/address_funds_transfer_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/address_funds_transfer_transition.rs index 03d12dd6c26..879df4477f1 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/address_funds_transfer_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/address_funds_transfer_transition.rs @@ -35,9 +35,9 @@ export interface AddressFundsTransferTransitionObject { } export interface AddressFundsTransferTransitionJSON { - inputs: object[]; - outputs: object[]; - feeStrategy: object[]; + inputs: PlatformAddressInputJSON[]; + outputs: PlatformAddressOutputJSON[]; + feeStrategy: FeeStrategyStepJSON[]; userFeeIncrease: number; } "#; @@ -79,7 +79,7 @@ impl AddressFundsTransferTransitionWasm { })? .unwrap_or(0); - let inputs_map = inputs.into_iter().map(|i| i.into_inner()).collect(); + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; let outputs_map = outputs_to_btree_map(outputs)?; let fee_strategy = fee_strategy_from_steps_or_default(fee_strategy); @@ -146,13 +146,14 @@ impl AddressFundsTransferTransitionWasm { } #[wasm_bindgen(setter = "inputs")] - pub fn set_inputs(&mut self, inputs: Vec) { - let inputs_map = inputs.into_iter().map(|i| i.into_inner()).collect(); + pub fn set_inputs(&mut self, inputs: Vec) -> WasmDppResult<()> { + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; match &mut self.0 { AddressFundsTransferTransition::V0(v0) => { v0.inputs = inputs_map; } } + Ok(()) } #[wasm_bindgen(getter = "outputs")] diff --git a/packages/wasm-dpp2/src/platform_address/transitions/identity_create_from_addresses_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/identity_create_from_addresses_transition.rs index 67976d670be..dcbd403468d 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/identity_create_from_addresses_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/identity_create_from_addresses_transition.rs @@ -41,10 +41,10 @@ export interface IdentityCreateFromAddressesTransitionObject { } export interface IdentityCreateFromAddressesTransitionJSON { - publicKeys: object[]; - inputs: object[]; - output?: object; - feeStrategy: object[]; + publicKeys: IdentityPublicKeyInCreationJSON[]; + inputs: PlatformAddressInputJSON[]; + output?: PlatformAddressOutputJSON; + feeStrategy: FeeStrategyStepJSON[]; userFeeIncrease: number; } "#; @@ -91,7 +91,7 @@ impl IdentityCreateFromAddressesTransitionWasm { })? .unwrap_or(0); - let inputs_map = inputs.into_iter().map(|i| i.into_inner()).collect(); + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; let output = output.map(|o| o.try_into_inner()).transpose()?; let fee_strategy = fee_strategy_from_steps_or_default(fee_strategy); @@ -188,13 +188,14 @@ impl IdentityCreateFromAddressesTransitionWasm { } #[wasm_bindgen(setter = "inputs")] - pub fn set_inputs(&mut self, inputs: Vec) { - let inputs_map = inputs.into_iter().map(|i| i.into_inner()).collect(); + pub fn set_inputs(&mut self, inputs: Vec) -> WasmDppResult<()> { + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; match &mut self.0 { IdentityCreateFromAddressesTransition::V0(v0) => { v0.inputs = inputs_map; } } + Ok(()) } #[wasm_bindgen(getter = "output")] diff --git a/packages/wasm-dpp2/src/platform_address/transitions/identity_credit_transfer_to_addresses_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/identity_credit_transfer_to_addresses_transition.rs index c949bba21bd..895d2efedbc 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/identity_credit_transfer_to_addresses_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/identity_credit_transfer_to_addresses_transition.rs @@ -34,7 +34,7 @@ export interface IdentityCreditTransferToAddressesOptions { * IdentityCreditTransferToAddresses serialized as a plain object. */ export interface IdentityCreditTransferToAddressesObject { - recipientAddresses: Array<{ address: Uint8Array; amount: bigint }>; + recipientAddresses: PlatformAddressOutputObject[]; senderId: Uint8Array; nonce: bigint; userFeeIncrease: number; @@ -46,7 +46,7 @@ export interface IdentityCreditTransferToAddressesObject { * IdentityCreditTransferToAddresses serialized as JSON. */ export interface IdentityCreditTransferToAddressesJSON { - recipientAddresses: Array<{ address: string; amount: string }>; + recipientAddresses: PlatformAddressOutputJSON[]; senderId: string; nonce: string; userFeeIncrease: number; diff --git a/packages/wasm-dpp2/src/platform_address/transitions/identity_top_up_from_addresses_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/identity_top_up_from_addresses_transition.rs index 4ca39908a26..7ad4d701822 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/identity_top_up_from_addresses_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/identity_top_up_from_addresses_transition.rs @@ -38,9 +38,9 @@ export interface IdentityTopUpFromAddressesTransitionObject { export interface IdentityTopUpFromAddressesTransitionJSON { identityId: string; - inputs: object[]; - output?: object; - feeStrategy: object[]; + inputs: PlatformAddressInputJSON[]; + output?: PlatformAddressOutputJSON; + feeStrategy: FeeStrategyStepJSON[]; userFeeIncrease: number; } "#; @@ -84,7 +84,7 @@ impl IdentityTopUpFromAddressesTransitionWasm { })? .unwrap_or(0); - let inputs_map = inputs.into_iter().map(|i| i.into_inner()).collect(); + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; let output = output.map(|o| o.try_into_inner()).transpose()?; let fee_strategy = fee_strategy_from_steps_or_default(fee_strategy); @@ -173,13 +173,14 @@ impl IdentityTopUpFromAddressesTransitionWasm { } #[wasm_bindgen(setter = "inputs")] - pub fn set_inputs(&mut self, inputs: Vec) { - let inputs_map = inputs.into_iter().map(|i| i.into_inner()).collect(); + pub fn set_inputs(&mut self, inputs: Vec) -> WasmDppResult<()> { + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; match &mut self.0 { IdentityTopUpFromAddressesTransition::V0(v0) => { v0.inputs = inputs_map; } } + Ok(()) } #[wasm_bindgen(getter = "output")] diff --git a/packages/wasm-dpp2/src/serialization/conversions.rs b/packages/wasm-dpp2/src/serialization/conversions.rs index 8a5c21df8a7..b7499672066 100644 --- a/packages/wasm-dpp2/src/serialization/conversions.rs +++ b/packages/wasm-dpp2/src/serialization/conversions.rs @@ -87,7 +87,11 @@ pub fn json_to_js_value(value: &JsonValue) -> WasmDppResult { /// - Recursively processes nested objects and arrays /// /// Performance: Uses fast path for primitives, only recursively processes objects/arrays. -fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult { +/// +/// `pub(crate)` so wrappers whose `to_object()` embeds JS `Map`s (e.g. +/// shielded proof-result wrappers backed by `js_sys::Map`) can call this in +/// their `to_json()` to get a `JSON.stringify`-friendly form. +pub(crate) fn normalize_js_value_for_json(value: &JsValue) -> WasmDppResult { // Fast path: primitives that can't contain BigInt or need conversion if value.is_string() || value.as_f64().is_some() diff --git a/packages/wasm-dpp2/src/shielded/address_witness.rs b/packages/wasm-dpp2/src/shielded/address_witness.rs new file mode 100644 index 00000000000..b782cc04874 --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/address_witness.rs @@ -0,0 +1,195 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_try_from_js_value; +use crate::impl_wasm_type_info; +use crate::utils::{IntoWasm, try_from_options_with, try_to_array}; +use dpp::address_funds::AddressWitness; +use dpp::platform_value::BinaryData; +use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const ADDRESS_WITNESS_TS_TYPES: &str = r#" +/** + * Address witness for P2PKH spending in Object form. + */ +export interface AddressWitnessP2pkhObject { + type: "p2pkh"; + signature: Uint8Array; +} + +/** + * Address witness for P2SH spending in Object form. + */ +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 in JSON form. + */ +export interface AddressWitnessP2pkhJSON { + type: "p2pkh"; + signature: string; +} + +/** + * Address witness for P2SH spending in JSON form. + */ +export interface AddressWitnessP2shJSON { + type: "p2sh"; + signatures: string[]; + redeemScript: string; +} + +/** + * Address witness (P2PKH or P2SH) in JSON form. + */ +export type AddressWitnessJSON = AddressWitnessP2pkhJSON | AddressWitnessP2shJSON; +"#; + +/// The input witness data required to spend from a PlatformAddress. +/// +/// Captures the different spending patterns for P2PKH (recoverable signature only) +/// and P2SH (signatures + redeem script) addresses. +#[wasm_bindgen(js_name = "AddressWitness")] +#[derive(Clone, Debug)] +pub struct AddressWitnessWasm(AddressWitness); + +impl From for AddressWitnessWasm { + fn from(w: AddressWitness) -> Self { + Self(w) + } +} + +impl From for AddressWitness { + fn from(w: AddressWitnessWasm) -> Self { + w.0 + } +} + +#[wasm_bindgen(js_class = AddressWitness)] +impl AddressWitnessWasm { + /// Creates a P2PKH witness from a recoverable ECDSA signature (typically 65 bytes + /// including the recovery byte prefix). + #[wasm_bindgen(js_name = "p2pkh")] + pub fn p2pkh(signature: Vec) -> AddressWitnessWasm { + AddressWitnessWasm(AddressWitness::P2pkh { + signature: BinaryData::new(signature), + }) + } + + /// Creates a P2SH witness from a list of signatures and the redeem script. + /// + /// For a 2-of-3 multisig, `signatures` would be `[OP_0, sig1, sig2]` and + /// `redeemScript` would be `OP_2 OP_3 OP_CHECKMULTISIG`. + /// + /// Each entry in `signatures` must be a `Uint8Array`. The signature count is + /// validated by DPP (`MAX_P2SH_SIGNATURES = 17`) on serialization; this + /// constructor does not duplicate that check. + #[wasm_bindgen(js_name = "p2sh")] + pub fn p2sh( + #[wasm_bindgen(unchecked_param_type = "Uint8Array[]")] signatures: js_sys::Array, + #[wasm_bindgen(js_name = "redeemScript")] redeem_script: Vec, + ) -> WasmDppResult { + let len = signatures.length() as usize; + let mut converted = Vec::with_capacity(len); + for (i, value) in signatures.iter().enumerate() { + let bytes: js_sys::Uint8Array = value.dyn_into().map_err(|_| { + WasmDppError::invalid_argument(format!( + "p2sh signatures[{}] must be a Uint8Array", + i + )) + })?; + converted.push(BinaryData::new(bytes.to_vec())); + } + Ok(AddressWitnessWasm(AddressWitness::P2sh { + signatures: converted, + redeem_script: BinaryData::new(redeem_script), + })) + } + + /// Returns the witness kind: "p2pkh" or "p2sh". + #[wasm_bindgen(getter)] + pub fn kind(&self) -> String { + match self.0 { + AddressWitness::P2pkh { .. } => "p2pkh".to_string(), + AddressWitness::P2sh { .. } => "p2sh".to_string(), + } + } + + /// Returns true if this is a P2PKH witness. + #[wasm_bindgen(js_name = "isP2pkh", getter)] + pub fn is_p2pkh(&self) -> bool { + self.0.is_p2pkh() + } + + /// Returns true if this is a P2SH witness. + #[wasm_bindgen(js_name = "isP2sh", getter)] + pub fn is_p2sh(&self) -> bool { + self.0.is_p2sh() + } + + /// Returns the signature bytes for a P2PKH witness, or `null` for P2SH. + #[wasm_bindgen(getter)] + pub fn signature(&self) -> Option> { + match &self.0 { + AddressWitness::P2pkh { signature } => Some(signature.to_vec()), + AddressWitness::P2sh { .. } => None, + } + } + + /// Returns the signatures for a P2SH witness, or `null` for P2PKH. + #[wasm_bindgen(getter)] + pub fn signatures(&self) -> Option> { + match &self.0 { + AddressWitness::P2sh { signatures, .. } => Some( + signatures + .iter() + .map(|sig| js_sys::Uint8Array::from(sig.as_slice())) + .collect(), + ), + AddressWitness::P2pkh { .. } => None, + } + } + + /// Returns the redeem script for a P2SH witness, or `null` for P2PKH. + #[wasm_bindgen(js_name = "redeemScript", getter)] + pub fn redeem_script(&self) -> Option> { + self.0.redeem_script().map(|s| s.to_vec()) + } +} + +impl_try_from_js_value!(AddressWitnessWasm, "AddressWitness"); +impl_wasm_type_info!(AddressWitnessWasm, AddressWitness); + +/// Extract a `Vec` from a JS options object property. +/// +/// Reads the named property as a JS array, then extracts each element as an +/// `AddressWitness` wasm-bindgen object via its internal pointer. +pub fn input_witnesses_from_js_options( + options: &JsValue, + field_name: &str, +) -> WasmDppResult> { + let array = try_from_options_with(options, field_name, |v| try_to_array(v, field_name))?; + array + .iter() + .enumerate() + .map(|(i, item)| { + item.to_wasm::("AddressWitness") + .map(|r| (*r).clone()) + .map_err(|_| { + WasmDppError::invalid_argument(format!( + "{}[{}] is not an AddressWitness", + field_name, i + )) + }) + }) + .collect() +} diff --git a/packages/wasm-dpp2/src/shielded/mod.rs b/packages/wasm-dpp2/src/shielded/mod.rs new file mode 100644 index 00000000000..799756e4057 --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/mod.rs @@ -0,0 +1,36 @@ +pub mod address_witness; +pub mod orchard_action; +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 address_witness::{AddressWitnessWasm, input_witnesses_from_js_options}; +pub use orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +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 crate::utils::try_vec_to_fixed_bytes; +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: Vec, + extra_data: &[u8], +) -> WasmDppResult> { + let commitment: [u8; 32] = try_vec_to_fixed_bytes(bundle_commitment, "bundleCommitment")?; + let result = dpp::shielded::compute_platform_sighash(&commitment, extra_data); + Ok(result.to_vec()) +} diff --git a/packages/wasm-dpp2/src/shielded/orchard_action.rs b/packages/wasm-dpp2/src/shielded/orchard_action.rs new file mode 100644 index 00000000000..cd131476890 --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/orchard_action.rs @@ -0,0 +1,192 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_try_from_js_value; +use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_type_info; +use crate::utils::{ + IntoWasm, try_from_options_with, try_to_array, try_to_bytes, try_to_fixed_bytes, +}; +use dpp::shielded::SerializedAction; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const SERIALIZED_ORCHARD_ACTION_TS_TYPES: &str = r#" +/** + * Options for constructing a SerializedOrchardAction. + */ +export interface SerializedOrchardActionOptions { + nullifier: Uint8Array; + rk: Uint8Array; + cmx: Uint8Array; + encryptedNote: Uint8Array; + cvNet: Uint8Array; + spendAuthSig: Uint8Array; +} + +/** + * A serialized Orchard action (spend-output pair) in Object form. + */ +export interface SerializedOrchardActionObject { + 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; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "SerializedOrchardActionOptions")] + pub type SerializedOrchardActionOptionsJs; + + #[wasm_bindgen(typescript_type = "SerializedOrchardActionObject")] + pub type SerializedOrchardActionObjectJs; + + #[wasm_bindgen(typescript_type = "SerializedOrchardActionJSON")] + pub type SerializedOrchardActionJSONJs; +} + +/// A serialized Orchard action: the on-chain representation of a spend-output pair. +/// +/// Each action consumes a previously created note (revealing its `nullifier`) while +/// creating a new note (publishing its commitment `cmx`). Privacy is preserved by +/// the zero-knowledge proof; observers cannot link spent notes to their commitments. +#[wasm_bindgen(js_name = "SerializedOrchardAction")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct SerializedOrchardActionWasm(SerializedAction); + +impl From for SerializedOrchardActionWasm { + fn from(action: SerializedAction) -> Self { + Self(action) + } +} + +impl From for SerializedAction { + fn from(wasm: SerializedOrchardActionWasm) -> Self { + wasm.0 + } +} + +#[wasm_bindgen(js_class = SerializedOrchardAction)] +impl SerializedOrchardActionWasm { + #[wasm_bindgen(constructor)] + pub fn constructor( + options: SerializedOrchardActionOptionsJs, + ) -> WasmDppResult { + let opts: &JsValue = options.as_ref(); + + let nullifier: [u8; 32] = try_from_options_with(opts, "nullifier", |v| { + try_to_fixed_bytes::<32>(v.clone(), "nullifier") + })?; + let rk: [u8; 32] = + try_from_options_with(opts, "rk", |v| try_to_fixed_bytes::<32>(v.clone(), "rk"))?; + let cmx: [u8; 32] = + try_from_options_with(opts, "cmx", |v| try_to_fixed_bytes::<32>(v.clone(), "cmx"))?; + // No size check — DPP's structural validation enforces + // `encrypted_note.len() == ENCRYPTED_NOTE_SIZE` (216 bytes); wasm-dpp2 + // is a thin TS convenience wrapper and doesn't duplicate DPP logic. + let encrypted_note: Vec = try_from_options_with(opts, "encryptedNote", |v| { + try_to_bytes(v.clone(), "encryptedNote") + })?; + let cv_net: [u8; 32] = try_from_options_with(opts, "cvNet", |v| { + try_to_fixed_bytes::<32>(v.clone(), "cvNet") + })?; + let spend_auth_sig: [u8; 64] = try_from_options_with(opts, "spendAuthSig", |v| { + try_to_fixed_bytes::<64>(v.clone(), "spendAuthSig") + })?; + + Ok(SerializedOrchardActionWasm(SerializedAction { + nullifier, + rk, + cmx, + encrypted_note, + cv_net, + spend_auth_sig, + })) + } + + /// Returns the 32-byte nullifier (note-spend tag). + #[wasm_bindgen(getter)] + pub fn nullifier(&self) -> Vec { + self.0.nullifier.to_vec() + } + + /// Returns the 32-byte randomized spend validating key. + #[wasm_bindgen(getter)] + pub fn rk(&self) -> Vec { + self.0.rk.to_vec() + } + + /// Returns the 32-byte note commitment for the new output note. + #[wasm_bindgen(getter)] + pub fn cmx(&self) -> Vec { + self.0.cmx.to_vec() + } + + /// Returns the 216-byte encrypted note ciphertext. + #[wasm_bindgen(getter = encryptedNote)] + pub fn encrypted_note(&self) -> Vec { + self.0.encrypted_note.clone() + } + + /// Returns the 32-byte value commitment. + #[wasm_bindgen(getter = cvNet)] + pub fn cv_net(&self) -> Vec { + self.0.cv_net.to_vec() + } + + /// Returns the 64-byte RedPallas spend authorization signature. + #[wasm_bindgen(getter = spendAuthSig)] + pub fn spend_auth_sig(&self) -> Vec { + self.0.spend_auth_sig.to_vec() + } +} + +impl_try_from_js_value!(SerializedOrchardActionWasm, "SerializedOrchardAction"); +impl_wasm_type_info!(SerializedOrchardActionWasm, SerializedOrchardAction); +impl_wasm_conversions_serde!( + SerializedOrchardActionWasm, + SerializedOrchardAction, + SerializedOrchardActionObjectJs, + SerializedOrchardActionJSONJs +); + +/// Extract a `Vec` from a JS options object property. +/// +/// Reads the named property as a JS array, then extracts each element as a +/// `SerializedOrchardAction` wasm-bindgen object via its internal pointer. +pub fn actions_from_js_options( + options: &JsValue, + field_name: &str, +) -> WasmDppResult> { + let array = try_from_options_with(options, field_name, |v| try_to_array(v, field_name))?; + array + .iter() + .enumerate() + .map(|(i, item)| { + item.to_wasm::("SerializedOrchardAction") + .map(|r| (*r).clone()) + .map_err(|_| { + WasmDppError::invalid_argument(format!( + "{}[{}] is not a SerializedOrchardAction", + field_name, i + )) + }) + }) + .collect() +} 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..9ca6f903a9e --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs @@ -0,0 +1,242 @@ +use crate::asset_lock_proof::AssetLockProofWasm; +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::utils::{try_from_options, try_vec_to_fixed_bytes}; +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::*; + +/// Non-WASM-instance fields extracted from the constructor options via serde. +/// +/// `actions` and `assetLockProof` are extracted separately as WASM class instances. +/// `signature` is optional because transitions are typically constructed unsigned. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ShieldFromAssetLockTransitionSimpleFields { + value_balance: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, + #[serde(default)] + signature: Vec, +} + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * 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: SerializedOrchardActionObject[]; + 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 WASM class instances (borrow &options) + let asset_lock: AssetLockProofWasm = try_from_options(&options, "assetLockProof")?; + let actions = actions_from_js_options(options.as_ref(), "actions")?; + + // Extract remaining simple fields via serde (consumes options) + let fields: ShieldFromAssetLockTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(ShieldFromAssetLockTransitionWasm( + ShieldFromAssetLockTransition::V0(ShieldFromAssetLockTransitionV0 { + asset_lock_proof: asset_lock.into(), + actions: actions.into_iter().map(Into::into).collect(), + value_balance: fields.value_balance, + anchor, + proof: fields.proof, + binding_signature, + signature: BinaryData::from(fields.signature), + }), + )) + } + + /// Returns the asset lock proof. + #[wasm_bindgen(getter = "assetLockProof")] + pub fn asset_lock_proof(&self) -> AssetLockProofWasm { + match &self.0 { + ShieldFromAssetLockTransition::V0(v0) => { + AssetLockProofWasm::from(v0.asset_lock_proof.clone()) + } + } + } + + /// Returns the serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + match &self.0 { + ShieldFromAssetLockTransition::V0(v0) => v0 + .actions + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect(), + } + } + + /// Returns the net value balance. + #[wasm_bindgen(getter = "valueBalance")] + pub fn value_balance(&self) -> u64 { + match &self.0 { + ShieldFromAssetLockTransition::V0(v0) => v0.value_balance, + } + } + + /// Returns the anchor (32-byte Merkle root). + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + match &self.0 { + ShieldFromAssetLockTransition::V0(v0) => v0.anchor.to_vec(), + } + } + + /// Returns the Halo2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + match &self.0 { + ShieldFromAssetLockTransition::V0(v0) => v0.proof.clone(), + } + } + + /// Returns the RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + match &self.0 { + ShieldFromAssetLockTransition::V0(v0) => v0.binding_signature.to_vec(), + } + } + + /// Returns the ECDSA signature. + #[wasm_bindgen(getter = "signature")] + pub fn 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..43576407a9f --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/shield_transition.rs @@ -0,0 +1,290 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::platform_address::{ + FeeStrategyStepWasm, PlatformAddressInputWasm, fee_strategy_from_js_options, + fee_strategy_from_steps_or_default, inputs_from_js_options, +}; +use crate::shielded::address_witness::{AddressWitnessWasm, input_witnesses_from_js_options}; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::utils::try_vec_to_fixed_bytes; +use crate::utils::{try_from_options_optional_with, try_to_u16}; +use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use dpp::prelude::UserFeeIncrease; +use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; +use dpp::state_transition::shield_transition::ShieldTransition; +use dpp::state_transition::shield_transition::v0::ShieldTransitionV0; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * Options for constructing a ShieldTransition. + * Uses WASM instance types for complex fields. + */ +export interface ShieldTransitionOptions { + inputs: PlatformAddressInput[]; + actions: SerializedOrchardAction[]; + amount: bigint; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; + feeStrategy?: FeeStrategyStep[]; + userFeeIncrease?: number; + inputWitnesses: AddressWitness[]; +} + +/** + * ShieldTransition serialized as a plain object. + */ +export interface ShieldTransitionObject { + $formatVersion: string; + inputs: PlatformAddressInputObject[]; + actions: SerializedOrchardActionObject[]; + 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: PlatformAddressInputJSON[]; + actions: SerializedOrchardActionJSON[]; + amount: number | string; + anchor: string; + proof: string; + bindingSignature: string; + feeStrategy: FeeStrategyStepJSON[]; + userFeeIncrease: number; + inputWitnesses: AddressWitnessJSON[]; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ShieldTransitionOptions")] + pub type ShieldTransitionOptionsJs; + + #[wasm_bindgen(typescript_type = "ShieldTransitionObject")] + pub type ShieldTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "ShieldTransitionJSON")] + pub type ShieldTransitionJSONJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +/// +/// The complex fields (`inputs`, `actions`, `feeStrategy`, `inputWitnesses`) are +/// extracted separately as WASM class instances. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ShieldTransitionSimpleFields { + amount: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[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(options: ShieldTransitionOptionsJs) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + + // Extract WASM class instances (borrow &options) + let inputs = inputs_from_js_options(js_opts, "inputs")?; + let actions = actions_from_js_options(js_opts, "actions")?; + let input_witnesses = input_witnesses_from_js_options(js_opts, "inputWitnesses")?; + let fee_strategy = fee_strategy_from_js_options(js_opts, "feeStrategy")?; + let user_fee_increase: UserFeeIncrease = + try_from_options_optional_with(js_opts, "userFeeIncrease", |v| { + try_to_u16(v, "userFeeIncrease") + })? + .unwrap_or(0); + + // Extract simple fields via serde (consumes options) + let fields: ShieldTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + let inputs_map = crate::platform_address::inputs_to_btree_map(inputs)?; + let fee_strategy = fee_strategy_from_steps_or_default(fee_strategy); + + Ok(ShieldTransitionWasm(ShieldTransition::V0( + ShieldTransitionV0 { + inputs: inputs_map, + actions: actions.into_iter().map(Into::into).collect(), + amount: fields.amount, + anchor, + proof: fields.proof, + binding_signature, + fee_strategy, + user_fee_increase, + input_witnesses: input_witnesses.into_iter().map(Into::into).collect(), + }, + ))) + } + + /// Returns the input addresses funding the shield (with their nonces and amounts). + #[wasm_bindgen(getter = "inputs")] + pub fn inputs(&self) -> Vec { + match &self.0 { + ShieldTransition::V0(v0) => v0 + .inputs + .iter() + .map(|(address, (nonce, amount))| { + PlatformAddressInputWasm::new(*address, *nonce, *amount) + }) + .collect(), + } + } + + /// Returns the serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + match &self.0 { + ShieldTransition::V0(v0) => v0 + .actions + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect(), + } + } + + /// Returns the shield amount (credits entering the pool). + #[wasm_bindgen(getter = "amount")] + pub fn amount(&self) -> u64 { + match &self.0 { + ShieldTransition::V0(v0) => v0.amount, + } + } + + /// Returns the anchor (32-byte Merkle root). + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + match &self.0 { + ShieldTransition::V0(v0) => v0.anchor.to_vec(), + } + } + + /// Returns the Halo2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + match &self.0 { + ShieldTransition::V0(v0) => v0.proof.clone(), + } + } + + /// Returns the RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + match &self.0 { + ShieldTransition::V0(v0) => v0.binding_signature.to_vec(), + } + } + + /// Returns the fee strategy steps. + #[wasm_bindgen(getter = "feeStrategy")] + pub fn fee_strategy(&self) -> Vec { + match &self.0 { + ShieldTransition::V0(v0) => v0 + .fee_strategy + .iter() + .cloned() + .map(FeeStrategyStepWasm::from) + .collect(), + } + } + + /// Returns the user fee increase multiplier. + #[wasm_bindgen(getter = "userFeeIncrease")] + pub fn user_fee_increase(&self) -> u16 { + match &self.0 { + ShieldTransition::V0(v0) => v0.user_fee_increase, + } + } + + /// Returns the input witnesses (signatures authorising each input). + #[wasm_bindgen(getter = "inputWitnesses")] + pub fn input_witnesses(&self) -> Vec { + match &self.0 { + ShieldTransition::V0(v0) => v0 + .input_witnesses + .iter() + .cloned() + .map(AddressWitnessWasm::from) + .collect(), + } + } + + #[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..6d8e18f78bc --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/shielded_transfer_transition.rs @@ -0,0 +1,202 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::utils::try_vec_to_fixed_bytes; +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::shielded_transfer_transition::v0::ShieldedTransferTransitionV0; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * Options for constructing a ShieldedTransferTransition. + */ +export interface ShieldedTransferTransitionOptions { + actions: SerializedOrchardAction[]; + valueBalance: bigint; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} + +/** + * ShieldedTransferTransition serialized as a plain object. + */ +export interface ShieldedTransferTransitionObject { + $formatVersion: string; + actions: SerializedOrchardActionObject[]; + 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 = "ShieldedTransferTransitionOptions")] + pub type ShieldedTransferTransitionOptionsJs; + + #[wasm_bindgen(typescript_type = "ShieldedTransferTransitionObject")] + pub type ShieldedTransferTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "ShieldedTransferTransitionJSON")] + pub type ShieldedTransferTransitionJSONJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ShieldedTransferTransitionSimpleFields { + value_balance: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[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( + options: ShieldedTransferTransitionOptionsJs, + ) -> WasmDppResult { + let actions = actions_from_js_options(options.as_ref(), "actions")?; + + let fields: ShieldedTransferTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(ShieldedTransferTransitionWasm( + ShieldedTransferTransition::V0(ShieldedTransferTransitionV0 { + actions: actions.into_iter().map(Into::into).collect(), + value_balance: fields.value_balance, + anchor, + proof: fields.proof, + binding_signature, + }), + )) + } + + /// Returns the serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + match &self.0 { + ShieldedTransferTransition::V0(v0) => v0 + .actions + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect(), + } + } + + /// Returns the value balance (fee amount leaving the pool). + #[wasm_bindgen(getter = "valueBalance")] + pub fn value_balance(&self) -> u64 { + match &self.0 { + ShieldedTransferTransition::V0(v0) => v0.value_balance, + } + } + + /// Returns the anchor (32-byte Merkle root). + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + match &self.0 { + ShieldedTransferTransition::V0(v0) => v0.anchor.to_vec(), + } + } + + /// Returns the Halo2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + match &self.0 { + ShieldedTransferTransition::V0(v0) => v0.proof.clone(), + } + } + + /// Returns the RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn 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..3f26faad96b --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/shielded_withdrawal_transition.rs @@ -0,0 +1,255 @@ +use crate::core::core_script::CoreScriptWasm; +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::identity::transitions::pooling::PoolingWasm; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::utils::try_from_options; +use crate::utils::try_vec_to_fixed_bytes; +use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use dpp::identity::core_script::CoreScript; +use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; +use dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition; +use dpp::state_transition::shielded_withdrawal_transition::v0::ShieldedWithdrawalTransitionV0; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * Options for constructing a ShieldedWithdrawalTransition. + * + * `pooling` accepts the `Pooling` enum, the lower-case name string + * ("never" / "ifavailable" / "standard"), or the numeric value (0/1/2) — same + * shape as IdentityCreditWithdrawalTransition. + */ +export interface ShieldedWithdrawalTransitionOptions { + actions: SerializedOrchardAction[]; + unshieldingAmount: bigint; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; + coreFeePerByte: number; + pooling: CreditWithdrawalTransitionPoolingLike; + outputScript: Uint8Array; +} + +/** + * ShieldedWithdrawalTransition serialized as a plain object. + */ +export interface ShieldedWithdrawalTransitionObject { + $formatVersion: string; + actions: SerializedOrchardActionObject[]; + unshieldingAmount: bigint; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; + coreFeePerByte: number; + pooling: PoolingWasm; + 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: string; + outputScript: string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ShieldedWithdrawalTransitionOptions")] + pub type ShieldedWithdrawalTransitionOptionsJs; + + #[wasm_bindgen(typescript_type = "ShieldedWithdrawalTransitionObject")] + pub type ShieldedWithdrawalTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "ShieldedWithdrawalTransitionJSON")] + pub type ShieldedWithdrawalTransitionJSONJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +/// `pooling` is extracted separately via `try_from_options` so it accepts the +/// flexible `PoolingLikeJs` shape (enum / string / number). +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ShieldedWithdrawalTransitionSimpleFields { + unshielding_amount: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, + core_fee_per_byte: u32, + output_script: Vec, +} + +#[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( + options: ShieldedWithdrawalTransitionOptionsJs, + ) -> WasmDppResult { + let actions = actions_from_js_options(options.as_ref(), "actions")?; + let pooling: PoolingWasm = try_from_options(options.as_ref(), "pooling")?; + + let fields: ShieldedWithdrawalTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(ShieldedWithdrawalTransitionWasm( + ShieldedWithdrawalTransition::V0(ShieldedWithdrawalTransitionV0 { + actions: actions.into_iter().map(Into::into).collect(), + unshielding_amount: fields.unshielding_amount, + anchor, + proof: fields.proof, + binding_signature, + core_fee_per_byte: fields.core_fee_per_byte, + pooling: pooling.into(), + output_script: CoreScript::from(fields.output_script), + }), + )) + } + + /// Returns the serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0 + .actions + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect(), + } + } + + /// Returns the unshielding amount. + #[wasm_bindgen(getter = "unshieldingAmount")] + pub fn unshielding_amount(&self) -> u64 { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.unshielding_amount, + } + } + + /// Returns the anchor (32-byte Merkle root). + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.anchor.to_vec(), + } + } + + /// Returns the Halo2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.proof.clone(), + } + } + + /// Returns the RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.binding_signature.to_vec(), + } + } + + /// Returns the core fee per byte. + #[wasm_bindgen(getter = "coreFeePerByte")] + pub fn core_fee_per_byte(&self) -> u32 { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.core_fee_per_byte, + } + } + + /// Returns the pooling strategy as a name string ("never" / "ifavailable" / "standard"). + /// Matches the shape of `IdentityCreditWithdrawalTransition.pooling`. + #[wasm_bindgen(getter = "pooling")] + pub fn pooling(&self) -> String { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => PoolingWasm::from(v0.pooling).into(), + } + } + + /// Returns the output script (core address). + #[wasm_bindgen(getter = "outputScript")] + pub fn output_script(&self) -> CoreScriptWasm { + match &self.0 { + ShieldedWithdrawalTransition::V0(v0) => v0.output_script.clone().into(), + } + } + + #[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..5873fa6eedd --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/unshield_transition.rs @@ -0,0 +1,219 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::platform_address::PlatformAddressWasm; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::utils::try_from_options; +use crate::utils::try_vec_to_fixed_bytes; +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::unshield_transition::v0::UnshieldTransitionV0; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * Options for constructing an UnshieldTransition. + */ +export interface UnshieldTransitionOptions { + outputAddress: PlatformAddressLike; + actions: SerializedOrchardAction[]; + unshieldingAmount: bigint; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} + +/** + * UnshieldTransition serialized as a plain object. + * + * `outputAddress` is the raw 21 bytes of a PlatformAddress (type byte + 20-byte hash); + * the JSON form (below) carries the same value as a hex string. + */ +export interface UnshieldTransitionObject { + $formatVersion: string; + outputAddress: Uint8Array; + actions: SerializedOrchardActionObject[]; + unshieldingAmount: bigint; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} + +/** + * UnshieldTransition serialized as JSON (human-readable). + */ +export interface UnshieldTransitionJSON { + $formatVersion: string; + outputAddress: string; + actions: SerializedOrchardActionJSON[]; + unshieldingAmount: number | string; + anchor: string; + proof: string; + bindingSignature: string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "UnshieldTransitionOptions")] + pub type UnshieldTransitionOptionsJs; + + #[wasm_bindgen(typescript_type = "UnshieldTransitionObject")] + pub type UnshieldTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "UnshieldTransitionJSON")] + pub type UnshieldTransitionJSONJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct UnshieldTransitionSimpleFields { + unshielding_amount: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[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(options: UnshieldTransitionOptionsJs) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + + let output_address: PlatformAddressWasm = try_from_options(js_opts, "outputAddress")?; + let actions = actions_from_js_options(js_opts, "actions")?; + + let fields: UnshieldTransitionSimpleFields = serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(UnshieldTransitionWasm(UnshieldTransition::V0( + UnshieldTransitionV0 { + output_address: output_address.into(), + actions: actions.into_iter().map(Into::into).collect(), + unshielding_amount: fields.unshielding_amount, + anchor, + proof: fields.proof, + binding_signature, + }, + ))) + } + + /// Returns the output address receiving the unshielded funds. + #[wasm_bindgen(getter = "outputAddress")] + pub fn output_address(&self) -> PlatformAddressWasm { + match &self.0 { + UnshieldTransition::V0(v0) => PlatformAddressWasm::from(v0.output_address), + } + } + + /// Returns the serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + match &self.0 { + UnshieldTransition::V0(v0) => v0 + .actions + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect(), + } + } + + /// Returns the unshielding amount. + #[wasm_bindgen(getter = "unshieldingAmount")] + pub fn unshielding_amount(&self) -> u64 { + match &self.0 { + UnshieldTransition::V0(v0) => v0.unshielding_amount, + } + } + + /// Returns the anchor (32-byte Merkle root). + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + match &self.0 { + UnshieldTransition::V0(v0) => v0.anchor.to_vec(), + } + } + + /// Returns the Halo2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + match &self.0 { + UnshieldTransition::V0(v0) => v0.proof.clone(), + } + } + + /// Returns the RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn 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/batch/document_transitions/create.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs index bb45538218f..7a79a1e7e94 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs @@ -1,5 +1,5 @@ use crate::data_contract::document::DocumentWasm; -use crate::error::{WasmDppError, WasmDppResult}; +use crate::error::WasmDppResult; use crate::impl_wasm_type_info; use crate::serialization; use crate::state_transitions::batch::document_base_transition::DocumentBaseTransitionWasm; @@ -9,7 +9,7 @@ use crate::state_transitions::batch::prefunded_voting_balance::PrefundedVotingBa use crate::state_transitions::batch::token_payment_info::TokenPaymentInfoWasm; use crate::utils::{ try_from_options, try_from_options_optional, try_from_options_with, try_to_u64, - ToSerdeJSONExt, + try_vec_to_fixed_bytes, ToSerdeJSONExt, }; use dpp::prelude::IdentityNonce; use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; @@ -118,15 +118,7 @@ impl DocumentCreateTransitionWasm { #[wasm_bindgen(setter = "entropy")] pub fn set_entropy(&mut self, entropy: Vec) -> WasmDppResult<()> { - if entropy.len() != 32 { - return Err(WasmDppError::invalid_argument(format!( - "Entropy must be exactly 32 bytes, got {}", - entropy.len() - ))); - } - let mut entropy_bytes = [0u8; 32]; - entropy_bytes.copy_from_slice(&entropy); - + let entropy_bytes: [u8; 32] = try_vec_to_fixed_bytes(entropy, "entropy")?; self.0.set_entropy(entropy_bytes); Ok(()) } diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result.rs b/packages/wasm-dpp2/src/state_transitions/proof_result.rs deleted file mode 100644 index 7faa3284dfb..00000000000 --- a/packages/wasm-dpp2/src/state_transitions/proof_result.rs +++ /dev/null @@ -1,930 +0,0 @@ -//! Typed WASM wrappers for `StateTransitionProofResult` variants. -//! -//! Each variant of the Rust `StateTransitionProofResult` enum gets its own -//! `#[wasm_bindgen]` struct with typed getters. On the TypeScript side they -//! are combined into a discriminated union `StateTransitionProofResultType` -//! (discriminated by the `__type` getter added via `impl_wasm_type_info!`). - -use crate::DataContractWasm; -use crate::DocumentWasm; -use crate::IdentifierWasm; -use crate::IdentityTokenInfoWasm; -use crate::IdentityWasm; -use crate::PartialIdentityWasm; -use crate::PlatformAddressWasm; -use crate::PlatformVersionLikeJs; -use crate::TokenStatusWasm; -use crate::VoteWasm; -use crate::data_contract::{DataContractJSONJs, DataContractObjectJs}; -use crate::error::{WasmDppError, WasmDppResult}; -use crate::impl_wasm_conversions_serde; -use crate::impl_wasm_type_info; -use crate::state_transitions::batch::token_pricing_schedule::TokenPricingScheduleWasm; -use crate::utils::JsMapExt; -use dpp::document::Document; -use dpp::platform_value::Identifier; -use dpp::state_transition::proof_result::StateTransitionProofResult; -use js_sys::{BigInt, Map}; -use serde::{Deserialize, Serialize}; -use wasm_bindgen::JsCast; -use wasm_bindgen::JsValue; -use wasm_bindgen::prelude::*; - -// ============================================================================ -// TypeScript union type -// ============================================================================ - -#[wasm_bindgen(typescript_custom_section)] -const TS_PROOF_RESULT_TYPE: &str = r#" -export type StateTransitionProofResultType = - | VerifiedDataContract - | VerifiedIdentity - | VerifiedTokenBalanceAbsence - | VerifiedTokenBalance - | VerifiedTokenIdentityInfo - | VerifiedTokenPricingSchedule - | VerifiedTokenStatus - | VerifiedTokenIdentitiesBalances - | VerifiedPartialIdentity - | VerifiedBalanceTransfer - | VerifiedDocuments - | VerifiedTokenActionWithDocument - | VerifiedTokenGroupActionWithDocument - | VerifiedTokenGroupActionWithTokenBalance - | VerifiedTokenGroupActionWithTokenIdentityInfo - | VerifiedTokenGroupActionWithTokenPricingSchedule - | VerifiedMasternodeVote - | VerifiedNextDistribution - | VerifiedAddressInfos - | VerifiedIdentityFullWithAddressInfos - | VerifiedIdentityWithAddressInfos; -"#; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(typescript_type = "StateTransitionProofResultType")] - pub type StateTransitionProofResultTypeJs; -} - -// ============================================================================ -// Helper: build a plain JS object from key-value pairs -// ============================================================================ - -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(); - } - obj.into() -} - -// ============================================================================ -// Variant structs -// ============================================================================ - -// --- VerifiedDataContract --- - -#[wasm_bindgen(js_name = "VerifiedDataContract")] -#[derive(Clone)] -pub struct VerifiedDataContractWasm { - #[wasm_bindgen(getter_with_clone, js_name = "dataContract")] - pub data_contract: DataContractWasm, -} - -#[wasm_bindgen(js_class = VerifiedDataContract)] -impl VerifiedDataContractWasm { - #[wasm_bindgen(js_name = toObject)] - pub fn to_object( - &self, - #[wasm_bindgen(js_name = "platformVersion")] platform_version: PlatformVersionLikeJs, - ) -> WasmDppResult { - let dc = self.data_contract.to_object(platform_version)?; - Ok(js_obj(&[("dataContract", dc.into())])) - } - - #[wasm_bindgen(js_name = toJSON)] - pub fn to_json( - &self, - #[wasm_bindgen(js_name = "platformVersion")] platform_version: PlatformVersionLikeJs, - ) -> WasmDppResult { - let dc = self.data_contract.to_json(platform_version)?; - Ok(js_obj(&[("dataContract", dc.into())])) - } - - #[wasm_bindgen(js_name = fromObject)] - pub fn from_object( - value: JsValue, - #[wasm_bindgen(js_name = "platformVersion")] platform_version: PlatformVersionLikeJs, - ) -> WasmDppResult { - let dc_val = js_sys::Reflect::get(&value, &"dataContract".into()) - .map_err(|_| WasmDppError::generic("Missing property: dataContract"))?; - let data_contract = DataContractWasm::from_object( - dc_val.unchecked_into::(), - false, - platform_version, - )?; - Ok(VerifiedDataContractWasm { data_contract }) - } - - #[wasm_bindgen(js_name = fromJSON)] - pub fn from_json( - value: JsValue, - #[wasm_bindgen(js_name = "platformVersion")] platform_version: PlatformVersionLikeJs, - ) -> WasmDppResult { - let dc_val = js_sys::Reflect::get(&value, &"dataContract".into()) - .map_err(|_| WasmDppError::generic("Missing property: dataContract"))?; - let data_contract = DataContractWasm::from_json( - dc_val.unchecked_into::(), - false, - platform_version, - )?; - Ok(VerifiedDataContractWasm { data_contract }) - } -} - -impl_wasm_type_info!(VerifiedDataContractWasm, VerifiedDataContract); - -// --- VerifiedIdentity --- - -#[wasm_bindgen(js_name = "VerifiedIdentity")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedIdentityWasm { - #[wasm_bindgen(getter_with_clone)] - pub identity: IdentityWasm, -} - -impl_wasm_type_info!(VerifiedIdentityWasm, VerifiedIdentity); -impl_wasm_conversions_serde!(VerifiedIdentityWasm, VerifiedIdentity); - -// --- VerifiedTokenBalanceAbsence --- - -#[wasm_bindgen(js_name = "VerifiedTokenBalanceAbsence")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedTokenBalanceAbsenceWasm { - #[wasm_bindgen(getter_with_clone, js_name = "tokenId")] - pub token_id: IdentifierWasm, -} - -impl_wasm_type_info!(VerifiedTokenBalanceAbsenceWasm, VerifiedTokenBalanceAbsence); -impl_wasm_conversions_serde!(VerifiedTokenBalanceAbsenceWasm, VerifiedTokenBalanceAbsence); - -// --- VerifiedTokenBalance --- - -#[dpp_json_convertible_derive::json_safe_fields(crate = "dpp")] -#[wasm_bindgen(js_name = "VerifiedTokenBalance")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedTokenBalanceWasm { - #[wasm_bindgen(getter_with_clone, js_name = "tokenId")] - pub token_id: IdentifierWasm, - balance: u64, -} - -#[wasm_bindgen(js_class = VerifiedTokenBalance)] -impl VerifiedTokenBalanceWasm { - #[wasm_bindgen(getter)] - pub fn balance(&self) -> JsValue { - BigInt::from(self.balance).into() - } -} - -impl_wasm_type_info!(VerifiedTokenBalanceWasm, VerifiedTokenBalance); -impl_wasm_conversions_serde!(VerifiedTokenBalanceWasm, VerifiedTokenBalance); - -// --- VerifiedTokenIdentityInfo --- - -#[wasm_bindgen(js_name = "VerifiedTokenIdentityInfo")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedTokenIdentityInfoWasm { - #[wasm_bindgen(getter_with_clone, js_name = "tokenId")] - pub token_id: IdentifierWasm, - #[wasm_bindgen(getter_with_clone, js_name = "tokenInfo")] - pub token_info: IdentityTokenInfoWasm, -} - -impl_wasm_type_info!(VerifiedTokenIdentityInfoWasm, VerifiedTokenIdentityInfo); -impl_wasm_conversions_serde!(VerifiedTokenIdentityInfoWasm, VerifiedTokenIdentityInfo); - -// --- VerifiedTokenPricingSchedule --- - -#[wasm_bindgen(js_name = "VerifiedTokenPricingSchedule")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedTokenPricingScheduleWasm { - #[wasm_bindgen(getter_with_clone, js_name = "tokenId")] - pub token_id: IdentifierWasm, - #[wasm_bindgen(getter_with_clone, js_name = "pricingSchedule")] - pub pricing_schedule: Option, -} - -impl_wasm_type_info!( - VerifiedTokenPricingScheduleWasm, - VerifiedTokenPricingSchedule -); -impl_wasm_conversions_serde!( - VerifiedTokenPricingScheduleWasm, - VerifiedTokenPricingSchedule -); - -// --- VerifiedTokenStatus --- - -#[wasm_bindgen(js_name = "VerifiedTokenStatus")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedTokenStatusWasm { - #[wasm_bindgen(getter_with_clone, js_name = "tokenStatus")] - pub token_status: TokenStatusWasm, -} - -impl_wasm_type_info!(VerifiedTokenStatusWasm, VerifiedTokenStatus); -impl_wasm_conversions_serde!(VerifiedTokenStatusWasm, VerifiedTokenStatus); - -// --- VerifiedTokenIdentitiesBalances --- - -#[wasm_bindgen(js_name = "VerifiedTokenIdentitiesBalances")] -#[derive(Clone)] -pub struct VerifiedTokenIdentitiesBalancesWasm { - balances: Map, // Map -} - -#[wasm_bindgen(js_class = VerifiedTokenIdentitiesBalances)] -impl VerifiedTokenIdentitiesBalancesWasm { - #[wasm_bindgen(getter)] - pub fn balances(&self) -> Map { - self.balances.clone() - } - - #[wasm_bindgen(js_name = toObject)] - pub fn to_object(&self) -> JsValue { - js_obj(&[("balances", self.balances.clone().into())]) - } - - #[wasm_bindgen(js_name = toJSON)] - pub fn to_json(&self) -> JsValue { - self.to_object() - } - - #[wasm_bindgen(js_name = fromObject)] - pub fn from_object(value: JsValue) -> WasmDppResult { - let map_val = js_sys::Reflect::get(&value, &"balances".into()) - .map_err(|_| WasmDppError::generic("Missing property: balances"))?; - Ok(VerifiedTokenIdentitiesBalancesWasm { - balances: map_val.unchecked_into(), - }) - } - - #[wasm_bindgen(js_name = fromJSON)] - pub fn from_json(value: JsValue) -> WasmDppResult { - Self::from_object(value) - } -} - -impl_wasm_type_info!( - VerifiedTokenIdentitiesBalancesWasm, - VerifiedTokenIdentitiesBalances -); - -// --- VerifiedPartialIdentity --- - -#[wasm_bindgen(js_name = "VerifiedPartialIdentity")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedPartialIdentityWasm { - #[wasm_bindgen(getter_with_clone, js_name = "partialIdentity")] - pub partial_identity: PartialIdentityWasm, -} - -impl_wasm_type_info!(VerifiedPartialIdentityWasm, VerifiedPartialIdentity); -impl_wasm_conversions_serde!(VerifiedPartialIdentityWasm, VerifiedPartialIdentity); - -// --- VerifiedBalanceTransfer --- - -#[wasm_bindgen(js_name = "VerifiedBalanceTransfer")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedBalanceTransferWasm { - #[wasm_bindgen(getter_with_clone)] - pub sender: PartialIdentityWasm, - #[wasm_bindgen(getter_with_clone)] - pub recipient: PartialIdentityWasm, -} - -impl_wasm_type_info!(VerifiedBalanceTransferWasm, VerifiedBalanceTransfer); -impl_wasm_conversions_serde!(VerifiedBalanceTransferWasm, VerifiedBalanceTransfer); - -// --- VerifiedDocuments --- - -#[wasm_bindgen(js_name = "VerifiedDocuments")] -#[derive(Clone)] -pub struct VerifiedDocumentsWasm { - documents: Map, // Map -} - -#[wasm_bindgen(js_class = VerifiedDocuments)] -impl VerifiedDocumentsWasm { - #[wasm_bindgen(getter)] - pub fn documents(&self) -> Map { - self.documents.clone() - } - - #[wasm_bindgen(js_name = toObject)] - pub fn to_object(&self) -> JsValue { - js_obj(&[("documents", self.documents.clone().into())]) - } - - #[wasm_bindgen(js_name = toJSON)] - pub fn to_json(&self) -> JsValue { - self.to_object() - } - - #[wasm_bindgen(js_name = fromObject)] - pub fn from_object(value: JsValue) -> WasmDppResult { - let map_val = js_sys::Reflect::get(&value, &"documents".into()) - .map_err(|_| WasmDppError::generic("Missing property: documents"))?; - Ok(VerifiedDocumentsWasm { - documents: map_val.unchecked_into(), - }) - } - - #[wasm_bindgen(js_name = fromJSON)] - pub fn from_json(value: JsValue) -> WasmDppResult { - Self::from_object(value) - } -} - -impl_wasm_type_info!(VerifiedDocumentsWasm, VerifiedDocuments); - -// --- VerifiedTokenActionWithDocument --- - -#[wasm_bindgen(js_name = "VerifiedTokenActionWithDocument")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedTokenActionWithDocumentWasm { - #[wasm_bindgen(getter_with_clone)] - pub document: DocumentWasm, -} - -impl_wasm_type_info!( - VerifiedTokenActionWithDocumentWasm, - VerifiedTokenActionWithDocument -); -impl_wasm_conversions_serde!( - VerifiedTokenActionWithDocumentWasm, - VerifiedTokenActionWithDocument -); - -// --- VerifiedTokenGroupActionWithDocument --- - -#[wasm_bindgen(js_name = "VerifiedTokenGroupActionWithDocument")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedTokenGroupActionWithDocumentWasm { - #[wasm_bindgen(getter_with_clone, js_name = "groupPower")] - pub group_power: u32, - #[wasm_bindgen(getter_with_clone)] - pub document: Option, -} - -impl_wasm_type_info!( - VerifiedTokenGroupActionWithDocumentWasm, - VerifiedTokenGroupActionWithDocument -); -impl_wasm_conversions_serde!( - VerifiedTokenGroupActionWithDocumentWasm, - VerifiedTokenGroupActionWithDocument -); - -// --- VerifiedTokenGroupActionWithTokenBalance --- - -#[wasm_bindgen(js_name = "VerifiedTokenGroupActionWithTokenBalance")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedTokenGroupActionWithTokenBalanceWasm { - #[wasm_bindgen(getter_with_clone, js_name = "groupPower")] - pub group_power: u32, - #[wasm_bindgen(getter_with_clone, js_name = "actionStatus")] - pub action_status: String, - balance: Option, -} - -#[wasm_bindgen(js_class = VerifiedTokenGroupActionWithTokenBalance)] -impl VerifiedTokenGroupActionWithTokenBalanceWasm { - #[wasm_bindgen(getter)] - pub fn balance(&self) -> JsValue { - match self.balance { - Some(b) => BigInt::from(b).into(), - None => JsValue::undefined(), - } - } -} - -impl_wasm_type_info!( - VerifiedTokenGroupActionWithTokenBalanceWasm, - VerifiedTokenGroupActionWithTokenBalance -); -impl_wasm_conversions_serde!( - VerifiedTokenGroupActionWithTokenBalanceWasm, - VerifiedTokenGroupActionWithTokenBalance -); - -// --- VerifiedTokenGroupActionWithTokenIdentityInfo --- - -#[wasm_bindgen(js_name = "VerifiedTokenGroupActionWithTokenIdentityInfo")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedTokenGroupActionWithTokenIdentityInfoWasm { - #[wasm_bindgen(getter_with_clone, js_name = "groupPower")] - pub group_power: u32, - #[wasm_bindgen(getter_with_clone, js_name = "actionStatus")] - pub action_status: String, - #[wasm_bindgen(getter_with_clone, js_name = "tokenInfo")] - pub token_info: Option, -} - -impl_wasm_type_info!( - VerifiedTokenGroupActionWithTokenIdentityInfoWasm, - VerifiedTokenGroupActionWithTokenIdentityInfo -); -impl_wasm_conversions_serde!( - VerifiedTokenGroupActionWithTokenIdentityInfoWasm, - VerifiedTokenGroupActionWithTokenIdentityInfo -); - -// --- VerifiedTokenGroupActionWithTokenPricingSchedule --- - -#[wasm_bindgen(js_name = "VerifiedTokenGroupActionWithTokenPricingSchedule")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedTokenGroupActionWithTokenPricingScheduleWasm { - #[wasm_bindgen(getter_with_clone, js_name = "groupPower")] - pub group_power: u32, - #[wasm_bindgen(getter_with_clone, js_name = "actionStatus")] - pub action_status: String, - #[wasm_bindgen(getter_with_clone, js_name = "pricingSchedule")] - pub pricing_schedule: Option, -} - -impl_wasm_type_info!( - VerifiedTokenGroupActionWithTokenPricingScheduleWasm, - VerifiedTokenGroupActionWithTokenPricingSchedule -); -impl_wasm_conversions_serde!( - VerifiedTokenGroupActionWithTokenPricingScheduleWasm, - VerifiedTokenGroupActionWithTokenPricingSchedule -); - -// --- VerifiedMasternodeVote --- - -#[wasm_bindgen(js_name = "VerifiedMasternodeVote")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedMasternodeVoteWasm { - #[wasm_bindgen(getter_with_clone)] - pub vote: VoteWasm, -} - -impl_wasm_type_info!(VerifiedMasternodeVoteWasm, VerifiedMasternodeVote); -impl_wasm_conversions_serde!(VerifiedMasternodeVoteWasm, VerifiedMasternodeVote); - -// --- VerifiedNextDistribution --- - -#[wasm_bindgen(js_name = "VerifiedNextDistribution")] -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifiedNextDistributionWasm { - #[wasm_bindgen(getter_with_clone)] - pub vote: VoteWasm, -} - -impl_wasm_type_info!(VerifiedNextDistributionWasm, VerifiedNextDistribution); -impl_wasm_conversions_serde!(VerifiedNextDistributionWasm, VerifiedNextDistribution); - -// --- VerifiedAddressInfos --- - -#[wasm_bindgen(js_name = "VerifiedAddressInfos")] -#[derive(Clone)] -pub struct VerifiedAddressInfosWasm { - address_infos: Map, // Map -} - -#[wasm_bindgen(js_class = VerifiedAddressInfos)] -impl VerifiedAddressInfosWasm { - #[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(&[("addressInfos", self.address_infos.clone().into())]) - } - - #[wasm_bindgen(js_name = toJSON)] - pub fn to_json(&self) -> JsValue { - self.to_object() - } - - #[wasm_bindgen(js_name = fromObject)] - pub fn from_object(value: JsValue) -> WasmDppResult { - let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) - .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; - Ok(VerifiedAddressInfosWasm { - address_infos: map_val.unchecked_into(), - }) - } - - #[wasm_bindgen(js_name = fromJSON)] - pub fn from_json(value: JsValue) -> WasmDppResult { - Self::from_object(value) - } -} - -impl_wasm_type_info!(VerifiedAddressInfosWasm, VerifiedAddressInfos); - -// --- VerifiedIdentityFullWithAddressInfos --- - -#[wasm_bindgen(js_name = "VerifiedIdentityFullWithAddressInfos")] -#[derive(Clone)] -pub struct VerifiedIdentityFullWithAddressInfosWasm { - #[wasm_bindgen(getter_with_clone)] - pub identity: IdentityWasm, - address_infos: Map, -} - -#[wasm_bindgen(js_class = VerifiedIdentityFullWithAddressInfos)] -impl VerifiedIdentityFullWithAddressInfosWasm { - #[wasm_bindgen(getter = "addressInfos")] - pub fn address_infos(&self) -> Map { - self.address_infos.clone() - } - - #[wasm_bindgen(js_name = toObject)] - pub fn to_object(&self) -> WasmDppResult { - let id = self.identity.to_object()?; - let map_js: JsValue = self.address_infos.clone().into(); - let obj = js_sys::Object::new(); - js_sys::Reflect::set(&obj, &"identity".into(), &id.into()).unwrap(); - js_sys::Reflect::set(&obj, &"addressInfos".into(), &map_js).unwrap(); - Ok(obj.into()) - } - - #[wasm_bindgen(js_name = toJSON)] - pub fn to_json(&self) -> WasmDppResult { - let id = self.identity.to_json()?; - let map_js: JsValue = self.address_infos.clone().into(); - let obj = js_sys::Object::new(); - js_sys::Reflect::set(&obj, &"identity".into(), &id.into()).unwrap(); - js_sys::Reflect::set(&obj, &"addressInfos".into(), &map_js).unwrap(); - Ok(obj.into()) - } - - #[wasm_bindgen(js_name = fromObject)] - pub fn from_object(value: JsValue) -> WasmDppResult { - let identity_val = js_sys::Reflect::get(&value, &"identity".into()) - .map_err(|_| WasmDppError::generic("Missing property: identity"))?; - let identity: IdentityWasm = crate::serialization::conversions::from_object(identity_val)?; - let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) - .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; - Ok(VerifiedIdentityFullWithAddressInfosWasm { - identity, - address_infos: map_val.unchecked_into(), - }) - } - - #[wasm_bindgen(js_name = fromJSON)] - pub fn from_json(value: JsValue) -> WasmDppResult { - let identity_val = js_sys::Reflect::get(&value, &"identity".into()) - .map_err(|_| WasmDppError::generic("Missing property: identity"))?; - let identity: IdentityWasm = crate::serialization::conversions::from_json(identity_val)?; - let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) - .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; - Ok(VerifiedIdentityFullWithAddressInfosWasm { - identity, - address_infos: map_val.unchecked_into(), - }) - } -} - -impl_wasm_type_info!( - VerifiedIdentityFullWithAddressInfosWasm, - VerifiedIdentityFullWithAddressInfos -); - -// --- VerifiedIdentityWithAddressInfos --- - -#[wasm_bindgen(js_name = "VerifiedIdentityWithAddressInfos")] -#[derive(Clone)] -pub struct VerifiedIdentityWithAddressInfosWasm { - #[wasm_bindgen(getter_with_clone, js_name = "partialIdentity")] - pub partial_identity: PartialIdentityWasm, - address_infos: Map, -} - -#[wasm_bindgen(js_class = VerifiedIdentityWithAddressInfos)] -impl VerifiedIdentityWithAddressInfosWasm { - #[wasm_bindgen(getter = "addressInfos")] - pub fn address_infos(&self) -> Map { - self.address_infos.clone() - } - - #[wasm_bindgen(js_name = toObject)] - pub fn to_object(&self) -> WasmDppResult { - let pi = self.partial_identity.to_object()?; - let map_js: JsValue = self.address_infos.clone().into(); - let obj = js_sys::Object::new(); - js_sys::Reflect::set(&obj, &"partialIdentity".into(), &pi.into()).unwrap(); - js_sys::Reflect::set(&obj, &"addressInfos".into(), &map_js).unwrap(); - Ok(obj.into()) - } - - #[wasm_bindgen(js_name = toJSON)] - pub fn to_json(&self) -> WasmDppResult { - let pi = self.partial_identity.to_json()?; - let map_js: JsValue = self.address_infos.clone().into(); - let obj = js_sys::Object::new(); - js_sys::Reflect::set(&obj, &"partialIdentity".into(), &pi.into()).unwrap(); - js_sys::Reflect::set(&obj, &"addressInfos".into(), &map_js).unwrap(); - Ok(obj.into()) - } - - #[wasm_bindgen(js_name = fromObject)] - pub fn from_object(value: JsValue) -> WasmDppResult { - let pi_val = js_sys::Reflect::get(&value, &"partialIdentity".into()) - .map_err(|_| WasmDppError::generic("Missing property: partialIdentity"))?; - let partial_identity: PartialIdentityWasm = - crate::serialization::conversions::from_object(pi_val)?; - let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) - .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; - Ok(VerifiedIdentityWithAddressInfosWasm { - partial_identity, - address_infos: map_val.unchecked_into(), - }) - } - - #[wasm_bindgen(js_name = fromJSON)] - pub fn from_json(value: JsValue) -> WasmDppResult { - let pi_val = js_sys::Reflect::get(&value, &"partialIdentity".into()) - .map_err(|_| WasmDppError::generic("Missing property: partialIdentity"))?; - let partial_identity: PartialIdentityWasm = - crate::serialization::conversions::from_json(pi_val)?; - let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) - .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; - Ok(VerifiedIdentityWithAddressInfosWasm { - partial_identity, - address_infos: map_val.unchecked_into(), - }) - } -} - -impl_wasm_type_info!( - VerifiedIdentityWithAddressInfosWasm, - VerifiedIdentityWithAddressInfos -); - -// ============================================================================ -// Conversion function -// ============================================================================ - -/// Wrap a raw `Document` into `DocumentWasm`. -/// -/// `DocumentWasm` requires metadata (contract ID, type name) that a bare -/// `Document` does not carry. When converting proof-result documents we -/// use empty defaults — the actual document data (id, owner_id, properties, -/// revision, timestamps) is fully preserved. -fn doc_to_wasm(doc: Document) -> DocumentWasm { - DocumentWasm::new(doc, Identifier::default(), String::new(), None) -} - -/// Helper to build `Map` -/// from the Rust address-info BTreeMap. Shared by three variants. -/// -/// Keys are hex-encoded PlatformAddress bytes so that JS consumers can -/// look up entries by string (JS Map uses reference equality for object keys). -fn build_address_infos_map( - map: std::collections::BTreeMap>, -) -> Map { - Map::from_entries(map.into_iter().map(|(address, info)| { - let address_wasm = PlatformAddressWasm::from(address); - let key: JsValue = address_wasm.to_hex().into(); - let val: JsValue = match info { - Some((nonce, credits)) => { - let obj = js_sys::Object::new(); - js_sys::Reflect::set(&obj, &"address".into(), &address_wasm.into()).unwrap(); - js_sys::Reflect::set(&obj, &"nonce".into(), &nonce.into()).unwrap(); - js_sys::Reflect::set(&obj, &"credits".into(), &BigInt::from(credits).into()) - .unwrap(); - obj.into() - } - None => JsValue::undefined(), - }; - (key, val) - })) -} - -fn action_status_to_string(status: dpp::group::group_action_status::GroupActionStatus) -> String { - match status { - dpp::group::group_action_status::GroupActionStatus::ActionActive => { - "ActionActive".to_string() - } - dpp::group::group_action_status::GroupActionStatus::ActionClosed => { - "ActionClosed".to_string() - } - } -} - -// --- 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(), - } - } -} -/// Convert a Rust `StateTransitionProofResult` into the corresponding typed -/// WASM wrapper, ready to be returned to JavaScript. -pub fn convert_proof_result( - result: StateTransitionProofResult, -) -> WasmDppResult { - let js_value: JsValue = match result { - StateTransitionProofResult::VerifiedDataContract(dc) => VerifiedDataContractWasm { - data_contract: dc.into(), - } - .into(), - - StateTransitionProofResult::VerifiedIdentity(identity) => VerifiedIdentityWasm { - identity: identity.into(), - } - .into(), - - StateTransitionProofResult::VerifiedTokenBalanceAbsence(id) => { - VerifiedTokenBalanceAbsenceWasm { - token_id: id.into(), - } - .into() - } - - StateTransitionProofResult::VerifiedTokenBalance(id, amount) => VerifiedTokenBalanceWasm { - token_id: id.into(), - balance: amount, - } - .into(), - - StateTransitionProofResult::VerifiedTokenIdentityInfo(id, info) => { - VerifiedTokenIdentityInfoWasm { - token_id: id.into(), - token_info: info.into(), - } - .into() - } - - StateTransitionProofResult::VerifiedTokenPricingSchedule(id, schedule) => { - VerifiedTokenPricingScheduleWasm { - token_id: id.into(), - pricing_schedule: schedule.map(Into::into), - } - .into() - } - - StateTransitionProofResult::VerifiedTokenStatus(status) => VerifiedTokenStatusWasm { - token_status: status.into(), - } - .into(), - - StateTransitionProofResult::VerifiedTokenIdentitiesBalances(balances) => { - let map = Map::from_entries(balances.into_iter().map(|(id, amount)| { - let key: JsValue = IdentifierWasm::from(id).to_base58().into(); - let val: JsValue = BigInt::from(amount).into(); - (key, val) - })); - VerifiedTokenIdentitiesBalancesWasm { balances: map }.into() - } - - StateTransitionProofResult::VerifiedPartialIdentity(pi) => VerifiedPartialIdentityWasm { - partial_identity: pi.into(), - } - .into(), - - StateTransitionProofResult::VerifiedBalanceTransfer(from, to) => { - VerifiedBalanceTransferWasm { - sender: from.into(), - recipient: to.into(), - } - .into() - } - - StateTransitionProofResult::VerifiedDocuments(docs) => { - let 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) - })); - VerifiedDocumentsWasm { documents: map }.into() - } - - StateTransitionProofResult::VerifiedTokenActionWithDocument(doc) => { - VerifiedTokenActionWithDocumentWasm { - document: doc_to_wasm(doc), - } - .into() - } - - StateTransitionProofResult::VerifiedTokenGroupActionWithDocument(power, maybe_doc) => { - VerifiedTokenGroupActionWithDocumentWasm { - group_power: power, - document: maybe_doc.map(doc_to_wasm), - } - .into() - } - - StateTransitionProofResult::VerifiedTokenGroupActionWithTokenBalance( - power, - status, - maybe_balance, - ) => VerifiedTokenGroupActionWithTokenBalanceWasm { - group_power: power, - action_status: action_status_to_string(status), - balance: maybe_balance, - } - .into(), - - StateTransitionProofResult::VerifiedTokenGroupActionWithTokenIdentityInfo( - power, - status, - maybe_info, - ) => VerifiedTokenGroupActionWithTokenIdentityInfoWasm { - group_power: power, - action_status: action_status_to_string(status), - token_info: maybe_info.map(Into::into), - } - .into(), - - StateTransitionProofResult::VerifiedTokenGroupActionWithTokenPricingSchedule( - power, - status, - maybe_schedule, - ) => VerifiedTokenGroupActionWithTokenPricingScheduleWasm { - group_power: power, - action_status: action_status_to_string(status), - pricing_schedule: maybe_schedule.map(Into::into), - } - .into(), - - StateTransitionProofResult::VerifiedMasternodeVote(vote) => { - VerifiedMasternodeVoteWasm { vote: vote.into() }.into() - } - - StateTransitionProofResult::VerifiedNextDistribution(vote) => { - VerifiedNextDistributionWasm { vote: vote.into() }.into() - } - - StateTransitionProofResult::VerifiedAddressInfos(infos) => VerifiedAddressInfosWasm { - address_infos: build_address_infos_map(infos), - } - .into(), - - StateTransitionProofResult::VerifiedIdentityFullWithAddressInfos(identity, infos) => { - VerifiedIdentityFullWithAddressInfosWasm { - identity: identity.into(), - address_infos: build_address_infos_map(infos), - } - .into() - } - - StateTransitionProofResult::VerifiedIdentityWithAddressInfos(pi, infos) => { - VerifiedIdentityWithAddressInfosWasm { - partial_identity: pi.into(), - address_infos: build_address_infos_map(infos), - } - .into() - } - - StateTransitionProofResult::VerifiedAssetLockConsumed(_) - | StateTransitionProofResult::VerifiedShieldedNullifiers(_) - | StateTransitionProofResult::VerifiedShieldedNullifiersWithAddressInfos(_, _) - | StateTransitionProofResult::VerifiedShieldedNullifiersWithWithdrawalDocument(_, _) => { - todo!("shielded proof results not yet implemented in wasm") - } - }; - - Ok(js_value.into()) -} diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/address_funds.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/address_funds.rs new file mode 100644 index 00000000000..71bb7f31302 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/address_funds.rs @@ -0,0 +1,200 @@ +//! Address-funds-related `StateTransitionProofResult` wrappers. + +use super::helpers::js_obj; +use crate::IdentityWasm; +use crate::PartialIdentityWasm; +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_wasm_type_info; +use js_sys::Map; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; + +// --- VerifiedAddressInfos --- + +#[wasm_bindgen(js_name = "VerifiedAddressInfos")] +#[derive(Clone)] +pub struct VerifiedAddressInfosWasm { + pub(super) address_infos: Map, // Map +} + +#[wasm_bindgen(js_class = VerifiedAddressInfos)] +impl VerifiedAddressInfosWasm { + #[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(&[("addressInfos", self.address_infos.clone().into())]) + } + + /// Returns a `JSON.stringify`-friendly form: the `Map` is normalised to a + /// plain object so its entries survive serialisation (otherwise + /// `JSON.stringify({addressInfos: })` produces `{"addressInfos":{}}`). + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> WasmDppResult { + crate::serialization::conversions::normalize_js_value_for_json(&self.to_object()) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object(value: JsValue) -> WasmDppResult { + let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) + .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; + Ok(VerifiedAddressInfosWasm { + address_infos: map_val.unchecked_into(), + }) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(value: JsValue) -> WasmDppResult { + Self::from_object(value) + } +} + +impl_wasm_type_info!(VerifiedAddressInfosWasm, VerifiedAddressInfos); + +// --- VerifiedIdentityFullWithAddressInfos --- + +#[wasm_bindgen(js_name = "VerifiedIdentityFullWithAddressInfos")] +#[derive(Clone)] +pub struct VerifiedIdentityFullWithAddressInfosWasm { + #[wasm_bindgen(getter_with_clone)] + pub identity: IdentityWasm, + pub(super) address_infos: Map, +} + +#[wasm_bindgen(js_class = VerifiedIdentityFullWithAddressInfos)] +impl VerifiedIdentityFullWithAddressInfosWasm { + #[wasm_bindgen(getter = "addressInfos")] + pub fn address_infos(&self) -> Map { + self.address_infos.clone() + } + + #[wasm_bindgen(js_name = toObject)] + pub fn to_object(&self) -> WasmDppResult { + let id = self.identity.to_object()?; + let map_js: JsValue = self.address_infos.clone().into(); + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"identity".into(), &id.into()).unwrap(); + js_sys::Reflect::set(&obj, &"addressInfos".into(), &map_js).unwrap(); + Ok(obj.into()) + } + + /// Returns a `JSON.stringify`-friendly form: the embedded `Map` is + /// normalised to a plain object so its entries survive serialisation. + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> WasmDppResult { + let id = self.identity.to_json()?; + let map_js: JsValue = self.address_infos.clone().into(); + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"identity".into(), &id.into()).unwrap(); + js_sys::Reflect::set(&obj, &"addressInfos".into(), &map_js).unwrap(); + crate::serialization::conversions::normalize_js_value_for_json(&obj.into()) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object(value: JsValue) -> WasmDppResult { + let identity_val = js_sys::Reflect::get(&value, &"identity".into()) + .map_err(|_| WasmDppError::generic("Missing property: identity"))?; + let identity: IdentityWasm = crate::serialization::conversions::from_object(identity_val)?; + let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) + .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; + Ok(VerifiedIdentityFullWithAddressInfosWasm { + identity, + address_infos: map_val.unchecked_into(), + }) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(value: JsValue) -> WasmDppResult { + let identity_val = js_sys::Reflect::get(&value, &"identity".into()) + .map_err(|_| WasmDppError::generic("Missing property: identity"))?; + let identity: IdentityWasm = crate::serialization::conversions::from_json(identity_val)?; + let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) + .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; + Ok(VerifiedIdentityFullWithAddressInfosWasm { + identity, + address_infos: map_val.unchecked_into(), + }) + } +} + +impl_wasm_type_info!( + VerifiedIdentityFullWithAddressInfosWasm, + VerifiedIdentityFullWithAddressInfos +); + +// --- VerifiedIdentityWithAddressInfos --- + +#[wasm_bindgen(js_name = "VerifiedIdentityWithAddressInfos")] +#[derive(Clone)] +pub struct VerifiedIdentityWithAddressInfosWasm { + #[wasm_bindgen(getter_with_clone, js_name = "partialIdentity")] + pub partial_identity: PartialIdentityWasm, + pub(super) address_infos: Map, +} + +#[wasm_bindgen(js_class = VerifiedIdentityWithAddressInfos)] +impl VerifiedIdentityWithAddressInfosWasm { + #[wasm_bindgen(getter = "addressInfos")] + pub fn address_infos(&self) -> Map { + self.address_infos.clone() + } + + #[wasm_bindgen(js_name = toObject)] + pub fn to_object(&self) -> WasmDppResult { + let pi = self.partial_identity.to_object()?; + let map_js: JsValue = self.address_infos.clone().into(); + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"partialIdentity".into(), &pi.into()).unwrap(); + js_sys::Reflect::set(&obj, &"addressInfos".into(), &map_js).unwrap(); + Ok(obj.into()) + } + + /// Returns a `JSON.stringify`-friendly form: the embedded `Map` is + /// normalised to a plain object so its entries survive serialisation. + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> WasmDppResult { + let pi = self.partial_identity.to_json()?; + let map_js: JsValue = self.address_infos.clone().into(); + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"partialIdentity".into(), &pi.into()).unwrap(); + js_sys::Reflect::set(&obj, &"addressInfos".into(), &map_js).unwrap(); + crate::serialization::conversions::normalize_js_value_for_json(&obj.into()) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object(value: JsValue) -> WasmDppResult { + let pi_val = js_sys::Reflect::get(&value, &"partialIdentity".into()) + .map_err(|_| WasmDppError::generic("Missing property: partialIdentity"))?; + let partial_identity: PartialIdentityWasm = + crate::serialization::conversions::from_object(pi_val)?; + let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) + .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; + Ok(VerifiedIdentityWithAddressInfosWasm { + partial_identity, + address_infos: map_val.unchecked_into(), + }) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(value: JsValue) -> WasmDppResult { + let pi_val = js_sys::Reflect::get(&value, &"partialIdentity".into()) + .map_err(|_| WasmDppError::generic("Missing property: partialIdentity"))?; + let partial_identity: PartialIdentityWasm = + crate::serialization::conversions::from_json(pi_val)?; + let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) + .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; + Ok(VerifiedIdentityWithAddressInfosWasm { + partial_identity, + address_infos: map_val.unchecked_into(), + }) + } +} + +impl_wasm_type_info!( + VerifiedIdentityWithAddressInfosWasm, + VerifiedIdentityWithAddressInfos +); diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs new file mode 100644 index 00000000000..85638e707db --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs @@ -0,0 +1,292 @@ +//! TypeScript discriminated-union declaration and the dispatcher that +//! converts a Rust `StateTransitionProofResult` into the corresponding +//! typed WASM wrapper. + +use super::address_funds::{ + VerifiedAddressInfosWasm, VerifiedIdentityFullWithAddressInfosWasm, + VerifiedIdentityWithAddressInfosWasm, +}; +use super::data_contract::VerifiedDataContractWasm; +use super::document::VerifiedDocumentsWasm; +use super::helpers::{ + action_status_to_string, build_address_infos_map, build_nullifier_map, doc_to_wasm, +}; +use super::identity::{ + VerifiedBalanceTransferWasm, VerifiedIdentityWasm, VerifiedPartialIdentityWasm, +}; +use super::shielded::{ + VerifiedAssetLockConsumedWasm, VerifiedShieldedNullifiersWasm, + VerifiedShieldedNullifiersWithAddressInfosWasm, + VerifiedShieldedNullifiersWithWithdrawalDocumentWasm, +}; +use super::token::{ + VerifiedTokenActionWithDocumentWasm, VerifiedTokenBalanceAbsenceWasm, VerifiedTokenBalanceWasm, + VerifiedTokenGroupActionWithDocumentWasm, VerifiedTokenGroupActionWithTokenBalanceWasm, + VerifiedTokenGroupActionWithTokenIdentityInfoWasm, + VerifiedTokenGroupActionWithTokenPricingScheduleWasm, VerifiedTokenIdentitiesBalancesWasm, + VerifiedTokenIdentityInfoWasm, VerifiedTokenPricingScheduleWasm, VerifiedTokenStatusWasm, +}; +use super::voting::{VerifiedMasternodeVoteWasm, VerifiedNextDistributionWasm}; +use crate::IdentifierWasm; +use crate::error::WasmDppResult; +use crate::utils::JsMapExt; +use dpp::state_transition::proof_result::StateTransitionProofResult; +use js_sys::{BigInt, Map}; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; + +// ============================================================================ +// TypeScript union type +// ============================================================================ + +#[wasm_bindgen(typescript_custom_section)] +const TS_PROOF_RESULT_TYPE: &str = r#" +export type StateTransitionProofResultType = + | VerifiedDataContract + | VerifiedIdentity + | VerifiedTokenBalanceAbsence + | VerifiedTokenBalance + | VerifiedTokenIdentityInfo + | VerifiedTokenPricingSchedule + | VerifiedTokenStatus + | VerifiedTokenIdentitiesBalances + | VerifiedPartialIdentity + | VerifiedBalanceTransfer + | VerifiedDocuments + | VerifiedTokenActionWithDocument + | VerifiedTokenGroupActionWithDocument + | VerifiedTokenGroupActionWithTokenBalance + | VerifiedTokenGroupActionWithTokenIdentityInfo + | VerifiedTokenGroupActionWithTokenPricingSchedule + | VerifiedMasternodeVote + | VerifiedNextDistribution + | VerifiedAddressInfos + | VerifiedIdentityFullWithAddressInfos + | VerifiedIdentityWithAddressInfos + | VerifiedAssetLockConsumed + | VerifiedShieldedNullifiers + | VerifiedShieldedNullifiersWithAddressInfos + | VerifiedShieldedNullifiersWithWithdrawalDocument; +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "StateTransitionProofResultType")] + pub type StateTransitionProofResultTypeJs; +} + +// ============================================================================ +// Conversion function +// ============================================================================ + +/// Convert a Rust `StateTransitionProofResult` into the corresponding typed +/// WASM wrapper, ready to be returned to JavaScript. +pub fn convert_proof_result( + result: StateTransitionProofResult, +) -> WasmDppResult { + let js_value: JsValue = match result { + StateTransitionProofResult::VerifiedDataContract(dc) => VerifiedDataContractWasm { + data_contract: dc.into(), + } + .into(), + + StateTransitionProofResult::VerifiedIdentity(identity) => VerifiedIdentityWasm { + identity: identity.into(), + } + .into(), + + StateTransitionProofResult::VerifiedTokenBalanceAbsence(id) => { + VerifiedTokenBalanceAbsenceWasm { + token_id: id.into(), + } + .into() + } + + StateTransitionProofResult::VerifiedTokenBalance(id, amount) => VerifiedTokenBalanceWasm { + token_id: id.into(), + balance: amount, + } + .into(), + + StateTransitionProofResult::VerifiedTokenIdentityInfo(id, info) => { + VerifiedTokenIdentityInfoWasm { + token_id: id.into(), + token_info: info.into(), + } + .into() + } + + StateTransitionProofResult::VerifiedTokenPricingSchedule(id, schedule) => { + VerifiedTokenPricingScheduleWasm { + token_id: id.into(), + pricing_schedule: schedule.map(Into::into), + } + .into() + } + + StateTransitionProofResult::VerifiedTokenStatus(status) => VerifiedTokenStatusWasm { + token_status: status.into(), + } + .into(), + + StateTransitionProofResult::VerifiedTokenIdentitiesBalances(balances) => { + let map = Map::from_entries(balances.into_iter().map(|(id, amount)| { + let key: JsValue = IdentifierWasm::from(id).to_base58().into(); + let val: JsValue = BigInt::from(amount).into(); + (key, val) + })); + VerifiedTokenIdentitiesBalancesWasm { balances: map }.into() + } + + StateTransitionProofResult::VerifiedPartialIdentity(pi) => VerifiedPartialIdentityWasm { + partial_identity: pi.into(), + } + .into(), + + StateTransitionProofResult::VerifiedBalanceTransfer(from, to) => { + VerifiedBalanceTransferWasm { + sender: from.into(), + recipient: to.into(), + } + .into() + } + + StateTransitionProofResult::VerifiedDocuments(docs) => { + let 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) + })); + VerifiedDocumentsWasm { documents: map }.into() + } + + StateTransitionProofResult::VerifiedTokenActionWithDocument(doc) => { + VerifiedTokenActionWithDocumentWasm { + document: doc_to_wasm(doc), + } + .into() + } + + StateTransitionProofResult::VerifiedTokenGroupActionWithDocument(power, maybe_doc) => { + VerifiedTokenGroupActionWithDocumentWasm { + group_power: power, + document: maybe_doc.map(doc_to_wasm), + } + .into() + } + + StateTransitionProofResult::VerifiedTokenGroupActionWithTokenBalance( + power, + status, + maybe_balance, + ) => VerifiedTokenGroupActionWithTokenBalanceWasm { + group_power: power, + action_status: action_status_to_string(status), + balance: maybe_balance, + } + .into(), + + StateTransitionProofResult::VerifiedTokenGroupActionWithTokenIdentityInfo( + power, + status, + maybe_info, + ) => VerifiedTokenGroupActionWithTokenIdentityInfoWasm { + group_power: power, + action_status: action_status_to_string(status), + token_info: maybe_info.map(Into::into), + } + .into(), + + StateTransitionProofResult::VerifiedTokenGroupActionWithTokenPricingSchedule( + power, + status, + maybe_schedule, + ) => VerifiedTokenGroupActionWithTokenPricingScheduleWasm { + group_power: power, + action_status: action_status_to_string(status), + pricing_schedule: maybe_schedule.map(Into::into), + } + .into(), + + StateTransitionProofResult::VerifiedMasternodeVote(vote) => { + VerifiedMasternodeVoteWasm { vote: vote.into() }.into() + } + + StateTransitionProofResult::VerifiedNextDistribution(vote) => { + VerifiedNextDistributionWasm { vote: vote.into() }.into() + } + + StateTransitionProofResult::VerifiedAddressInfos(infos) => VerifiedAddressInfosWasm { + address_infos: build_address_infos_map(infos), + } + .into(), + + StateTransitionProofResult::VerifiedIdentityFullWithAddressInfos(identity, infos) => { + VerifiedIdentityFullWithAddressInfosWasm { + identity: identity.into(), + address_infos: build_address_infos_map(infos), + } + .into() + } + + StateTransitionProofResult::VerifiedIdentityWithAddressInfos(pi, infos) => { + VerifiedIdentityWithAddressInfosWasm { + partial_identity: pi.into(), + address_infos: build_address_infos_map(infos), + } + .into() + } + + 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() + } + }; + + Ok(js_value.into()) +} diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/data_contract.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/data_contract.rs new file mode 100644 index 00000000000..f45a18686c4 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/data_contract.rs @@ -0,0 +1,71 @@ +//! `VerifiedDataContract` proof-result wrapper. + +use super::helpers::js_obj; +use crate::DataContractWasm; +use crate::PlatformVersionLikeJs; +use crate::data_contract::{DataContractJSONJs, DataContractObjectJs}; +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_wasm_type_info; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = "VerifiedDataContract")] +#[derive(Clone)] +pub struct VerifiedDataContractWasm { + #[wasm_bindgen(getter_with_clone, js_name = "dataContract")] + pub data_contract: DataContractWasm, +} + +#[wasm_bindgen(js_class = VerifiedDataContract)] +impl VerifiedDataContractWasm { + #[wasm_bindgen(js_name = toObject)] + pub fn to_object( + &self, + #[wasm_bindgen(js_name = "platformVersion")] platform_version: PlatformVersionLikeJs, + ) -> WasmDppResult { + let dc = self.data_contract.to_object(platform_version)?; + Ok(js_obj(&[("dataContract", dc.into())])) + } + + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json( + &self, + #[wasm_bindgen(js_name = "platformVersion")] platform_version: PlatformVersionLikeJs, + ) -> WasmDppResult { + let dc = self.data_contract.to_json(platform_version)?; + Ok(js_obj(&[("dataContract", dc.into())])) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object( + value: JsValue, + #[wasm_bindgen(js_name = "platformVersion")] platform_version: PlatformVersionLikeJs, + ) -> WasmDppResult { + let dc_val = js_sys::Reflect::get(&value, &"dataContract".into()) + .map_err(|_| WasmDppError::generic("Missing property: dataContract"))?; + let data_contract = DataContractWasm::from_object( + dc_val.unchecked_into::(), + false, + platform_version, + )?; + Ok(VerifiedDataContractWasm { data_contract }) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json( + value: JsValue, + #[wasm_bindgen(js_name = "platformVersion")] platform_version: PlatformVersionLikeJs, + ) -> WasmDppResult { + let dc_val = js_sys::Reflect::get(&value, &"dataContract".into()) + .map_err(|_| WasmDppError::generic("Missing property: dataContract"))?; + let data_contract = DataContractWasm::from_json( + dc_val.unchecked_into::(), + false, + platform_version, + )?; + Ok(VerifiedDataContractWasm { data_contract }) + } +} + +impl_wasm_type_info!(VerifiedDataContractWasm, VerifiedDataContract); diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/document.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/document.rs new file mode 100644 index 00000000000..1f83ef4df9f --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/document.rs @@ -0,0 +1,52 @@ +//! `VerifiedDocuments` proof-result wrapper. + +use super::helpers::js_obj; +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_wasm_type_info; +use js_sys::Map; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = "VerifiedDocuments")] +#[derive(Clone)] +pub struct VerifiedDocumentsWasm { + pub(super) documents: Map, // Map +} + +#[wasm_bindgen(js_class = VerifiedDocuments)] +impl VerifiedDocumentsWasm { + #[wasm_bindgen(getter)] + pub fn documents(&self) -> Map { + self.documents.clone() + } + + #[wasm_bindgen(js_name = toObject)] + pub fn to_object(&self) -> JsValue { + js_obj(&[("documents", self.documents.clone().into())]) + } + + /// Returns a `JSON.stringify`-friendly form: the `Map` is normalised to a + /// plain object so its entries survive serialisation (otherwise + /// `JSON.stringify({documents: })` produces `{"documents":{}}`). + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> WasmDppResult { + crate::serialization::conversions::normalize_js_value_for_json(&self.to_object()) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object(value: JsValue) -> WasmDppResult { + let map_val = js_sys::Reflect::get(&value, &"documents".into()) + .map_err(|_| WasmDppError::generic("Missing property: documents"))?; + Ok(VerifiedDocumentsWasm { + documents: map_val.unchecked_into(), + }) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(value: JsValue) -> WasmDppResult { + Self::from_object(value) + } +} + +impl_wasm_type_info!(VerifiedDocumentsWasm, VerifiedDocuments); diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs new file mode 100644 index 00000000000..9370946ee91 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs @@ -0,0 +1,79 @@ +//! Internal helpers shared across the `proof_result` submodules. +//! +//! These items are `pub(super)` only — they are not part of the public WASM +//! surface and exist purely to keep the per-domain modules DRY. + +use crate::DocumentWasm; +use crate::PlatformAddressWasm; +use crate::utils::JsMapExt; +use dpp::document::Document; +use dpp::platform_value::Identifier; +use js_sys::{BigInt, Map}; +use wasm_bindgen::JsValue; + +/// Build a plain JS object from key-value pairs. +pub(super) 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(); + } + obj.into() +} + +/// Wrap a raw `Document` into `DocumentWasm`. +/// +/// `DocumentWasm` requires metadata (contract ID, type name) that a bare +/// `Document` does not carry. When converting proof-result documents we +/// use empty defaults — the actual document data (id, owner_id, properties, +/// revision, timestamps) is fully preserved. +pub(super) fn doc_to_wasm(doc: Document) -> DocumentWasm { + DocumentWasm::new(doc, Identifier::default(), String::new(), None) +} + +/// Helper to build `Map` +/// from the Rust address-info BTreeMap. Shared by three variants. +/// +/// Keys are hex-encoded PlatformAddress bytes so that JS consumers can +/// look up entries by string (JS Map uses reference equality for object keys). +pub(super) fn build_address_infos_map( + map: std::collections::BTreeMap>, +) -> Map { + Map::from_entries(map.into_iter().map(|(address, info)| { + let address_wasm = PlatformAddressWasm::from(address); + let key: JsValue = address_wasm.to_hex().into(); + let val: JsValue = match info { + Some((nonce, credits)) => { + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"address".into(), &address_wasm.into()).unwrap(); + js_sys::Reflect::set(&obj, &"nonce".into(), &nonce.into()).unwrap(); + js_sys::Reflect::set(&obj, &"credits".into(), &BigInt::from(credits).into()) + .unwrap(); + obj.into() + } + None => JsValue::undefined(), + }; + (key, val) + })) +} + +/// Helper to build `Map` from shielded nullifier results. +pub(super) 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) + })) +} + +pub(super) fn action_status_to_string( + status: dpp::group::group_action_status::GroupActionStatus, +) -> String { + match status { + dpp::group::group_action_status::GroupActionStatus::ActionActive => { + "ActionActive".to_string() + } + dpp::group::group_action_status::GroupActionStatus::ActionClosed => { + "ActionClosed".to_string() + } + } +} diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/identity.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/identity.rs new file mode 100644 index 00000000000..f8d1d508385 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/identity.rs @@ -0,0 +1,52 @@ +//! Identity-related `StateTransitionProofResult` wrappers. +//! +//! Contains `VerifiedIdentity`, `VerifiedPartialIdentity`, and +//! `VerifiedBalanceTransfer`. + +use crate::IdentityWasm; +use crate::PartialIdentityWasm; +use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_type_info; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +// --- VerifiedIdentity --- + +#[wasm_bindgen(js_name = "VerifiedIdentity")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedIdentityWasm { + #[wasm_bindgen(getter_with_clone)] + pub identity: IdentityWasm, +} + +impl_wasm_type_info!(VerifiedIdentityWasm, VerifiedIdentity); +impl_wasm_conversions_serde!(VerifiedIdentityWasm, VerifiedIdentity); + +// --- VerifiedPartialIdentity --- + +#[wasm_bindgen(js_name = "VerifiedPartialIdentity")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedPartialIdentityWasm { + #[wasm_bindgen(getter_with_clone, js_name = "partialIdentity")] + pub partial_identity: PartialIdentityWasm, +} + +impl_wasm_type_info!(VerifiedPartialIdentityWasm, VerifiedPartialIdentity); +impl_wasm_conversions_serde!(VerifiedPartialIdentityWasm, VerifiedPartialIdentity); + +// --- VerifiedBalanceTransfer --- + +#[wasm_bindgen(js_name = "VerifiedBalanceTransfer")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedBalanceTransferWasm { + #[wasm_bindgen(getter_with_clone)] + pub sender: PartialIdentityWasm, + #[wasm_bindgen(getter_with_clone)] + pub recipient: PartialIdentityWasm, +} + +impl_wasm_type_info!(VerifiedBalanceTransferWasm, VerifiedBalanceTransfer); +impl_wasm_conversions_serde!(VerifiedBalanceTransferWasm, VerifiedBalanceTransfer); diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/mod.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/mod.rs new file mode 100644 index 00000000000..a48c0eb5793 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/mod.rs @@ -0,0 +1,18 @@ +mod address_funds; +mod convert; +mod data_contract; +mod document; +mod helpers; +mod identity; +mod shielded; +mod token; +mod voting; + +pub use address_funds::*; +pub use convert::*; +pub use data_contract::*; +pub use document::*; +pub use identity::*; +pub use shielded::*; +pub use token::*; +pub use voting::*; 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..efec44d6c7b --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/shielded.rs @@ -0,0 +1,277 @@ +//! Shielded pool WASM wrappers for `StateTransitionProofResult` variants. +//! +//! These types were extracted from `proof_result` to keep shielded-specific +//! code in its own module. + +use super::helpers::js_obj; +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_type_info; +use crate::serialization::conversions::normalize_js_value_for_json; +use js_sys::{BigInt, Map}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; + +fn read_map_property(value: &JsValue, name: &str) -> WasmDppResult { + let raw = js_sys::Reflect::get(value, &name.into()) + .map_err(|_| WasmDppError::generic(format!("Missing property: {}", name)))?; + Ok(raw.unchecked_into()) +} + +// --- 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())]) + } + + /// Returns a `JSON.stringify`-friendly form: the `Map` is normalised to a + /// plain object so its entries survive serialisation (otherwise + /// `JSON.stringify({nullifiers: })` produces `{"nullifiers":{}}`). + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> WasmDppResult { + normalize_js_value_for_json(&self.to_object()) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object(value: JsValue) -> WasmDppResult { + Ok(VerifiedShieldedNullifiersWasm { + nullifiers: read_map_property(&value, "nullifiers")?, + }) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(value: JsValue) -> WasmDppResult { + Self::from_object(value) + } +} + +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()), + ]) + } + + /// Returns a `JSON.stringify`-friendly form: the `Map` instances are + /// normalised to plain objects so their entries survive serialisation. + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> WasmDppResult { + normalize_js_value_for_json(&self.to_object()) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object( + value: JsValue, + ) -> WasmDppResult { + Ok(VerifiedShieldedNullifiersWithAddressInfosWasm { + nullifiers: read_map_property(&value, "nullifiers")?, + address_infos: read_map_property(&value, "addressInfos")?, + }) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json( + value: JsValue, + ) -> WasmDppResult { + Self::from_object(value) + } +} + +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()), + ]) + } + + /// Returns a `JSON.stringify`-friendly form: the `Map` instances are + /// normalised to plain objects so their entries survive serialisation. + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> WasmDppResult { + normalize_js_value_for_json(&self.to_object()) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object( + value: JsValue, + ) -> WasmDppResult { + Ok(VerifiedShieldedNullifiersWithWithdrawalDocumentWasm { + nullifiers: read_map_property(&value, "nullifiers")?, + documents: read_map_property(&value, "documents")?, + }) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json( + value: JsValue, + ) -> WasmDppResult { + Self::from_object(value) + } +} + +impl VerifiedShieldedNullifiersWithWithdrawalDocumentWasm { + pub fn new(nullifiers: Map, documents: Map) -> Self { + Self { + nullifiers, + documents, + } + } +} + +impl_wasm_type_info!( + VerifiedShieldedNullifiersWithWithdrawalDocumentWasm, + VerifiedShieldedNullifiersWithWithdrawalDocument +); diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs new file mode 100644 index 00000000000..a1e820e0237 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs @@ -0,0 +1,268 @@ +//! Token-related `StateTransitionProofResult` wrappers. + +use super::helpers::js_obj; +use crate::DocumentWasm; +use crate::IdentifierWasm; +use crate::IdentityTokenInfoWasm; +use crate::TokenStatusWasm; +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_type_info; +use crate::state_transitions::batch::token_pricing_schedule::TokenPricingScheduleWasm; +use js_sys::{BigInt, Map}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; + +// --- VerifiedTokenBalanceAbsence --- + +#[wasm_bindgen(js_name = "VerifiedTokenBalanceAbsence")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenBalanceAbsenceWasm { + #[wasm_bindgen(getter_with_clone, js_name = "tokenId")] + pub token_id: IdentifierWasm, +} + +impl_wasm_type_info!(VerifiedTokenBalanceAbsenceWasm, VerifiedTokenBalanceAbsence); +impl_wasm_conversions_serde!(VerifiedTokenBalanceAbsenceWasm, VerifiedTokenBalanceAbsence); + +// --- VerifiedTokenBalance --- + +#[dpp_json_convertible_derive::json_safe_fields(crate = "dpp")] +#[wasm_bindgen(js_name = "VerifiedTokenBalance")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenBalanceWasm { + #[wasm_bindgen(getter_with_clone, js_name = "tokenId")] + pub token_id: IdentifierWasm, + pub(super) balance: u64, +} + +#[wasm_bindgen(js_class = VerifiedTokenBalance)] +impl VerifiedTokenBalanceWasm { + #[wasm_bindgen(getter)] + pub fn balance(&self) -> JsValue { + BigInt::from(self.balance).into() + } +} + +impl_wasm_type_info!(VerifiedTokenBalanceWasm, VerifiedTokenBalance); +impl_wasm_conversions_serde!(VerifiedTokenBalanceWasm, VerifiedTokenBalance); + +// --- VerifiedTokenIdentityInfo --- + +#[wasm_bindgen(js_name = "VerifiedTokenIdentityInfo")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenIdentityInfoWasm { + #[wasm_bindgen(getter_with_clone, js_name = "tokenId")] + pub token_id: IdentifierWasm, + #[wasm_bindgen(getter_with_clone, js_name = "tokenInfo")] + pub token_info: IdentityTokenInfoWasm, +} + +impl_wasm_type_info!(VerifiedTokenIdentityInfoWasm, VerifiedTokenIdentityInfo); +impl_wasm_conversions_serde!(VerifiedTokenIdentityInfoWasm, VerifiedTokenIdentityInfo); + +// --- VerifiedTokenPricingSchedule --- + +#[wasm_bindgen(js_name = "VerifiedTokenPricingSchedule")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenPricingScheduleWasm { + #[wasm_bindgen(getter_with_clone, js_name = "tokenId")] + pub token_id: IdentifierWasm, + #[wasm_bindgen(getter_with_clone, js_name = "pricingSchedule")] + pub pricing_schedule: Option, +} + +impl_wasm_type_info!( + VerifiedTokenPricingScheduleWasm, + VerifiedTokenPricingSchedule +); +impl_wasm_conversions_serde!( + VerifiedTokenPricingScheduleWasm, + VerifiedTokenPricingSchedule +); + +// --- VerifiedTokenStatus --- + +#[wasm_bindgen(js_name = "VerifiedTokenStatus")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenStatusWasm { + #[wasm_bindgen(getter_with_clone, js_name = "tokenStatus")] + pub token_status: TokenStatusWasm, +} + +impl_wasm_type_info!(VerifiedTokenStatusWasm, VerifiedTokenStatus); +impl_wasm_conversions_serde!(VerifiedTokenStatusWasm, VerifiedTokenStatus); + +// --- VerifiedTokenIdentitiesBalances --- + +#[wasm_bindgen(js_name = "VerifiedTokenIdentitiesBalances")] +#[derive(Clone)] +pub struct VerifiedTokenIdentitiesBalancesWasm { + pub(super) balances: Map, // Map +} + +#[wasm_bindgen(js_class = VerifiedTokenIdentitiesBalances)] +impl VerifiedTokenIdentitiesBalancesWasm { + #[wasm_bindgen(getter)] + pub fn balances(&self) -> Map { + self.balances.clone() + } + + #[wasm_bindgen(js_name = toObject)] + pub fn to_object(&self) -> JsValue { + js_obj(&[("balances", self.balances.clone().into())]) + } + + /// Returns a `JSON.stringify`-friendly form: the `Map` is normalised to a + /// plain object so its entries survive serialisation (otherwise + /// `JSON.stringify({balances: })` produces `{"balances":{}}`). + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> WasmDppResult { + crate::serialization::conversions::normalize_js_value_for_json(&self.to_object()) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object(value: JsValue) -> WasmDppResult { + let map_val = js_sys::Reflect::get(&value, &"balances".into()) + .map_err(|_| WasmDppError::generic("Missing property: balances"))?; + Ok(VerifiedTokenIdentitiesBalancesWasm { + balances: map_val.unchecked_into(), + }) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(value: JsValue) -> WasmDppResult { + Self::from_object(value) + } +} + +impl_wasm_type_info!( + VerifiedTokenIdentitiesBalancesWasm, + VerifiedTokenIdentitiesBalances +); + +// --- VerifiedTokenActionWithDocument --- + +#[wasm_bindgen(js_name = "VerifiedTokenActionWithDocument")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenActionWithDocumentWasm { + #[wasm_bindgen(getter_with_clone)] + pub document: DocumentWasm, +} + +impl_wasm_type_info!( + VerifiedTokenActionWithDocumentWasm, + VerifiedTokenActionWithDocument +); +impl_wasm_conversions_serde!( + VerifiedTokenActionWithDocumentWasm, + VerifiedTokenActionWithDocument +); + +// --- VerifiedTokenGroupActionWithDocument --- + +#[wasm_bindgen(js_name = "VerifiedTokenGroupActionWithDocument")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenGroupActionWithDocumentWasm { + #[wasm_bindgen(getter_with_clone, js_name = "groupPower")] + pub group_power: u32, + #[wasm_bindgen(getter_with_clone)] + pub document: Option, +} + +impl_wasm_type_info!( + VerifiedTokenGroupActionWithDocumentWasm, + VerifiedTokenGroupActionWithDocument +); +impl_wasm_conversions_serde!( + VerifiedTokenGroupActionWithDocumentWasm, + VerifiedTokenGroupActionWithDocument +); + +// --- VerifiedTokenGroupActionWithTokenBalance --- + +#[wasm_bindgen(js_name = "VerifiedTokenGroupActionWithTokenBalance")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenGroupActionWithTokenBalanceWasm { + #[wasm_bindgen(getter_with_clone, js_name = "groupPower")] + pub group_power: u32, + #[wasm_bindgen(getter_with_clone, js_name = "actionStatus")] + pub action_status: String, + pub(super) balance: Option, +} + +#[wasm_bindgen(js_class = VerifiedTokenGroupActionWithTokenBalance)] +impl VerifiedTokenGroupActionWithTokenBalanceWasm { + #[wasm_bindgen(getter)] + pub fn balance(&self) -> JsValue { + match self.balance { + Some(b) => BigInt::from(b).into(), + None => JsValue::undefined(), + } + } +} + +impl_wasm_type_info!( + VerifiedTokenGroupActionWithTokenBalanceWasm, + VerifiedTokenGroupActionWithTokenBalance +); +impl_wasm_conversions_serde!( + VerifiedTokenGroupActionWithTokenBalanceWasm, + VerifiedTokenGroupActionWithTokenBalance +); + +// --- VerifiedTokenGroupActionWithTokenIdentityInfo --- + +#[wasm_bindgen(js_name = "VerifiedTokenGroupActionWithTokenIdentityInfo")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenGroupActionWithTokenIdentityInfoWasm { + #[wasm_bindgen(getter_with_clone, js_name = "groupPower")] + pub group_power: u32, + #[wasm_bindgen(getter_with_clone, js_name = "actionStatus")] + pub action_status: String, + #[wasm_bindgen(getter_with_clone, js_name = "tokenInfo")] + pub token_info: Option, +} + +impl_wasm_type_info!( + VerifiedTokenGroupActionWithTokenIdentityInfoWasm, + VerifiedTokenGroupActionWithTokenIdentityInfo +); +impl_wasm_conversions_serde!( + VerifiedTokenGroupActionWithTokenIdentityInfoWasm, + VerifiedTokenGroupActionWithTokenIdentityInfo +); + +// --- VerifiedTokenGroupActionWithTokenPricingSchedule --- + +#[wasm_bindgen(js_name = "VerifiedTokenGroupActionWithTokenPricingSchedule")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenGroupActionWithTokenPricingScheduleWasm { + #[wasm_bindgen(getter_with_clone, js_name = "groupPower")] + pub group_power: u32, + #[wasm_bindgen(getter_with_clone, js_name = "actionStatus")] + pub action_status: String, + #[wasm_bindgen(getter_with_clone, js_name = "pricingSchedule")] + pub pricing_schedule: Option, +} + +impl_wasm_type_info!( + VerifiedTokenGroupActionWithTokenPricingScheduleWasm, + VerifiedTokenGroupActionWithTokenPricingSchedule +); +impl_wasm_conversions_serde!( + VerifiedTokenGroupActionWithTokenPricingScheduleWasm, + VerifiedTokenGroupActionWithTokenPricingSchedule +); diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/voting.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/voting.rs new file mode 100644 index 00000000000..53fd729d578 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/voting.rs @@ -0,0 +1,33 @@ +//! Voting-related `StateTransitionProofResult` wrappers. + +use crate::VoteWasm; +use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_type_info; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +// --- VerifiedMasternodeVote --- + +#[wasm_bindgen(js_name = "VerifiedMasternodeVote")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedMasternodeVoteWasm { + #[wasm_bindgen(getter_with_clone)] + pub vote: VoteWasm, +} + +impl_wasm_type_info!(VerifiedMasternodeVoteWasm, VerifiedMasternodeVote); +impl_wasm_conversions_serde!(VerifiedMasternodeVoteWasm, VerifiedMasternodeVote); + +// --- VerifiedNextDistribution --- + +#[wasm_bindgen(js_name = "VerifiedNextDistribution")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedNextDistributionWasm { + #[wasm_bindgen(getter_with_clone)] + pub vote: VoteWasm, +} + +impl_wasm_type_info!(VerifiedNextDistributionWasm, VerifiedNextDistribution); +impl_wasm_conversions_serde!(VerifiedNextDistributionWasm, VerifiedNextDistribution); diff --git a/packages/wasm-dpp2/src/utils.rs b/packages/wasm-dpp2/src/utils.rs index 256f72ec352..472a5d2b70b 100644 --- a/packages/wasm-dpp2/src/utils.rs +++ b/packages/wasm-dpp2/src/utils.rs @@ -488,17 +488,43 @@ pub fn try_to_fixed_bytes( field_name: &str, ) -> WasmDppResult<[u8; N]> { let bytes = try_to_bytes(value, field_name)?; - if bytes.len() != N { - return Err(WasmDppError::invalid_argument(format!( - "'{}' must be exactly {} bytes, got {}", + try_vec_to_fixed_bytes(bytes, field_name) +} + +/// Convert an already-extracted `Vec` into a fixed-size `[u8; N]`, returning +/// a uniform error when the length doesn't match. +/// +/// Use this whenever you already hold the bytes (after `try_to_bytes`, after a +/// hex/base64 decode, after pulling them out of an inner DPP type, etc.) and +/// need the fixed-size array form. +pub fn try_vec_to_fixed_bytes( + bytes: Vec, + field_name: &str, +) -> WasmDppResult<[u8; N]> { + bytes.try_into().map_err(|original: Vec| { + WasmDppError::invalid_argument(format!( + "{} must be exactly {} bytes, got {}", field_name, N, + original.len() + )) + }) +} + +/// Reject byte-string fields whose length exceeds a sane upper bound, before +/// they flow into downstream code. Lets us short-circuit DoS-shaped inputs +/// (e.g. `serde_wasm_bindgen` happily accepting any iterable of u8s and +/// allocating before any structural check runs). +pub fn check_max_len(bytes: &[u8], max: usize, field_name: &str) -> WasmDppResult<()> { + if bytes.len() > max { + return Err(WasmDppError::invalid_argument(format!( + "{} must be at most {} bytes, got {}", + field_name, + max, bytes.len() ))); } - let mut arr = [0u8; N]; - arr.copy_from_slice(&bytes); - Ok(arr) + Ok(()) } /// Convert a JS value to u32 with validation. diff --git a/packages/wasm-dpp2/tests/unit/AddressCreditWithdrawalTransition.spec.ts b/packages/wasm-dpp2/tests/unit/AddressCreditWithdrawalTransition.spec.ts index e8815b4ab21..9c3272327c8 100644 --- a/packages/wasm-dpp2/tests/unit/AddressCreditWithdrawalTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/AddressCreditWithdrawalTransition.spec.ts @@ -192,6 +192,81 @@ describe('AddressCreditWithdrawalTransition', () => { }); }); + describe('toObject() / toJSON() / fromObject() / fromJSON()', () => { + it('toObject() emits inputs as typed array of {address, nonce, amount}', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.inputs).to.be.an('array').with.lengthOf(1); + expect(obj.inputs[0].address).to.be.instanceOf(Uint8Array); + expect(obj.inputs[0].address.length).to.equal(21); + expect(obj.inputs[0].nonce).to.equal(0); + expect(obj.inputs[0].amount).to.equal(BigInt(100000)); + }); + + it('toObject() emits output as typed singular {address, amount}', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.output).to.be.an('object'); + expect(obj.output.address).to.be.instanceOf(Uint8Array); + expect(obj.output.address.length).to.equal(21); + expect(obj.output.amount).to.equal(BigInt(90000)); + }); + + it('toObject() omits output when not provided', () => { + const inputAddr = wasm.PlatformAddress.fromBytes(addr1Bytes); + const script = wasm.CoreScript.fromP2PKH([ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ]); + const input = new wasm.PlatformAddressInput(inputAddr, 0, BigInt(100000)); + const transition = new wasm.AddressCreditWithdrawalTransition({ + inputs: [input], + outputScript: script, + pooling: 'never', + coreFeePerByte: 1, + }); + + const obj = transition.toObject(); + expect(obj.output == null).to.be.true(); // null OR undefined + }); + + it('toObject() emits feeStrategy with {type, index} shape', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.feeStrategy).to.be.an('array'); + expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].index).to.be.a('number'); + }); + + it('toJSON() emits hex addresses, string outputScript, and pooling name', () => { + const transition = createTransition(); + const json = transition.toJSON(); + + expect(json.inputs[0].address).to.be.a('string').with.lengthOf(42); + expect(json.output).to.be.an('object'); + expect(json.output.address).to.be.a('string').with.lengthOf(42); + expect(json.output.amount).to.satisfy((v: unknown) => typeof v === 'number' || typeof v === 'string'); + expect(json.outputScript).to.be.a('string'); + expect(json.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + }); + + it('fromObject(toObject()) round-trips identically', () => { + const transition = createTransition(); + const obj = transition.toObject(); + const restored = wasm.AddressCreditWithdrawalTransition.fromObject(obj); + expect(restored.toObject()).to.deep.equal(obj); + }); + + it('fromJSON(toJSON()) round-trips identically', () => { + const transition = createTransition(); + const json = transition.toJSON(); + const restored = wasm.AddressCreditWithdrawalTransition.fromJSON(json); + expect(restored.toJSON()).to.deep.equal(json); + }); + }); + describe('toStateTransition() / fromStateTransition()', () => { it('should convert to and from StateTransition', () => { const transition = createTransition(); diff --git a/packages/wasm-dpp2/tests/unit/AddressFundingFromAssetLockTransition.spec.ts b/packages/wasm-dpp2/tests/unit/AddressFundingFromAssetLockTransition.spec.ts index af62d43feeb..9d160146cd7 100644 --- a/packages/wasm-dpp2/tests/unit/AddressFundingFromAssetLockTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/AddressFundingFromAssetLockTransition.spec.ts @@ -115,7 +115,7 @@ describe('AddressFundingFromAssetLockTransition', () => { const transition = createTransition(); const proof = transition.assetLockProof; expect(proof).to.exist(); - expect(proof.lockTypeName).to.equal('Instant'); + expect(proof.lockType).to.equal('instant'); }); it('should set asset lock proof', () => { @@ -127,7 +127,7 @@ describe('AddressFundingFromAssetLockTransition', () => { const chainProof = wasm.AssetLockProof.createChainAssetLockProof(11, outpoint); transition.assetLockProof = chainProof; - expect(transition.assetLockProof.lockTypeName).to.equal('Chain'); + expect(transition.assetLockProof.lockType).to.equal('chain'); }); }); @@ -185,6 +185,102 @@ describe('AddressFundingFromAssetLockTransition', () => { }); }); + describe('toObject() / toJSON() / fromObject() / fromJSON()', () => { + it('toObject() emits inputs as typed array of {address, nonce, amount}', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.inputs).to.be.an('array'); + expect(obj.inputs).to.have.lengthOf(1); + expect(obj.inputs[0].address).to.be.instanceOf(Uint8Array); + expect(obj.inputs[0].address.length).to.equal(21); + expect(obj.inputs[0].nonce).to.equal(0); + expect(obj.inputs[0].amount).to.equal(BigInt(100000)); + }); + + it('toObject() emits outputs with absent amount for unspecified', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.outputs).to.be.an('array'); + expect(obj.outputs).to.have.lengthOf(1); + expect(obj.outputs[0].address).to.be.instanceOf(Uint8Array); + expect(obj.outputs[0].address.length).to.equal(21); + // serde Option::None becomes undefined in the wasm Object form (JSON form is null). + expect(obj.outputs[0].amount == null).to.be.true(); // null OR undefined + }); + + it('toObject() emits outputs with explicit bigint amount when set', () => { + const inputAddr = wasm.PlatformAddress.fromBytes(addr1Bytes); + const outputAddr = wasm.PlatformAddress.fromBytes(addr2Bytes); + const input = new wasm.PlatformAddressInput(inputAddr, 0, BigInt(50000)); + const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(40000)); + const transition = new wasm.AddressFundingFromAssetLockTransition({ + assetLockProof: createAssetLockProof(), + inputs: [input], + outputs: [output], + }); + + const obj = transition.toObject(); + expect(obj.outputs[0].amount).to.equal(BigInt(40000)); + }); + + it('toObject() emits feeStrategy as typed array of {type, index}', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.feeStrategy).to.be.an('array'); + expect(obj.feeStrategy.length).to.be.greaterThan(0); + expect(obj.feeStrategy[0]).to.have.property('type'); + expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].index).to.be.a('number'); + }); + + it('toJSON() emits inputs as typed array with hex address and number/string amount', () => { + const transition = createTransition(); + const json = transition.toJSON(); + + expect(json.inputs).to.be.an('array'); + expect(json.inputs[0].address).to.be.a('string'); + expect(json.inputs[0].address).to.have.lengthOf(42); // 21 bytes hex-encoded + expect(json.inputs[0].nonce).to.equal(0); + expect(json.inputs[0].amount).to.satisfy((v: unknown) => typeof v === 'number' || typeof v === 'string'); + }); + + it('toJSON() emits outputs with hex address and null amount when unset', () => { + const transition = createTransition(); + const json = transition.toJSON(); + + expect(json.outputs).to.be.an('array'); + expect(json.outputs[0].address).to.be.a('string'); + expect(json.outputs[0].address).to.have.lengthOf(42); + expect(json.outputs[0].amount).to.be.null(); + }); + + it('toJSON() emits feeStrategy with {type, index} shape', () => { + const transition = createTransition(); + const json = transition.toJSON(); + + expect(json.feeStrategy).to.be.an('array'); + expect(json.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(json.feeStrategy[0].index).to.be.a('number'); + }); + + it('fromObject(toObject()) round-trips identically', () => { + const transition = createTransition(); + const obj = transition.toObject(); + const restored = wasm.AddressFundingFromAssetLockTransition.fromObject(obj); + expect(restored.toObject()).to.deep.equal(obj); + }); + + it('fromJSON(toJSON()) round-trips identically', () => { + const transition = createTransition(); + const json = transition.toJSON(); + const restored = wasm.AddressFundingFromAssetLockTransition.fromJSON(json); + expect(restored.toJSON()).to.deep.equal(json); + }); + }); + describe('toStateTransition() / fromStateTransition()', () => { it('should convert to and from StateTransition', () => { const transition = createTransition(); diff --git a/packages/wasm-dpp2/tests/unit/AddressFundsTransferTransition.spec.ts b/packages/wasm-dpp2/tests/unit/AddressFundsTransferTransition.spec.ts index 510cd160b88..b3913b1a4e5 100644 --- a/packages/wasm-dpp2/tests/unit/AddressFundsTransferTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/AddressFundsTransferTransition.spec.ts @@ -156,6 +156,67 @@ describe('AddressFundsTransferTransition', () => { }); }); + describe('toObject() / toJSON() / fromObject() / fromJSON()', () => { + it('toObject() emits inputs as typed array of {address, nonce, amount}', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.inputs).to.be.an('array').with.lengthOf(1); + expect(obj.inputs[0].address).to.be.instanceOf(Uint8Array); + expect(obj.inputs[0].address.length).to.equal(21); + expect(obj.inputs[0].nonce).to.equal(0); + expect(obj.inputs[0].amount).to.equal(BigInt(100000)); + }); + + it('toObject() emits outputs as typed array of {address, amount} (required)', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.outputs).to.be.an('array').with.lengthOf(1); + expect(obj.outputs[0].address).to.be.instanceOf(Uint8Array); + expect(obj.outputs[0].address.length).to.equal(21); + expect(obj.outputs[0].amount).to.equal(BigInt(90000)); + }); + + it('toObject() emits feeStrategy with {type, index} shape', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.feeStrategy).to.be.an('array'); + expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].index).to.be.a('number'); + }); + + it('toJSON() emits inputs/outputs with hex address and number/string amount', () => { + const transition = createTransition(); + const json = transition.toJSON(); + + expect(json.inputs[0].address).to.be.a('string').with.lengthOf(42); + expect(json.inputs[0].nonce).to.equal(0); + expect(json.inputs[0].amount).to.satisfy((v: unknown) => typeof v === 'number' || typeof v === 'string'); + + expect(json.outputs[0].address).to.be.a('string').with.lengthOf(42); + expect(json.outputs[0].amount).to.satisfy((v: unknown) => typeof v === 'number' || typeof v === 'string'); + + expect(json.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(json.feeStrategy[0].index).to.be.a('number'); + }); + + it('fromObject(toObject()) round-trips identically', () => { + const transition = createTransition(); + const obj = transition.toObject(); + const restored = wasm.AddressFundsTransferTransition.fromObject(obj); + expect(restored.toObject()).to.deep.equal(obj); + }); + + it('fromJSON(toJSON()) round-trips identically', () => { + const transition = createTransition(); + const json = transition.toJSON(); + const restored = wasm.AddressFundsTransferTransition.fromJSON(json); + expect(restored.toJSON()).to.deep.equal(json); + }); + }); + describe('toStateTransition() / fromStateTransition()', () => { it('should convert to and from StateTransition', () => { const transition = createTransition(); diff --git a/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.ts b/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.ts index 289819ceaaa..ee4976b9eb4 100644 --- a/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.ts +++ b/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.ts @@ -139,7 +139,7 @@ describe('AssetLockProof', () => { expect(restoredProof.toObject()).to.deep.equal(objectRepresentation); }); - it('should export binary fields as Uint8Array in object form with type field', () => { + it('should export internally-tagged {type:"instant", ...fields} for Instant', () => { const instantLockProof = wasm.AssetLockProof.createInstantAssetLockProof( instantLockBytes, transactionBytes, @@ -148,15 +148,14 @@ describe('AssetLockProof', () => { const objectRepresentation = instantLockProof.toObject(); - // Should have type field (0 = Instant) - expect(objectRepresentation.type).to.equal(0); + expect(objectRepresentation.type).to.equal('instant'); expect(objectRepresentation.instantLock).to.be.instanceOf(Uint8Array); expect(objectRepresentation.transaction).to.be.instanceOf(Uint8Array); expect(objectRepresentation.instantLock).to.deep.equal(instantLockBytes); expect(objectRepresentation.transaction).to.deep.equal(transactionBytes); }); - it('should export chain lock proof object with type field', () => { + it('should export internally-tagged {type:"chain", ...fields} for Chain', () => { const outpoint = new wasm.OutPoint( 'e8b43025641eea4fd21190f01bd870ef90f1a8b199d8fc3376c5b62c0b1a179d', 1, @@ -165,16 +164,14 @@ describe('AssetLockProof', () => { const objectRepresentation = chainLockProof.toObject(); - // Should have type field (1 = Chain) - expect(objectRepresentation.type).to.equal(1); + expect(objectRepresentation.type).to.equal('chain'); expect(objectRepresentation.coreChainLockedHeight).to.equal(1); - // outPoint is {txid, vout} object expect(objectRepresentation.outPoint).to.be.an('object'); expect(objectRepresentation.outPoint.txid).to.exist(); expect(objectRepresentation.outPoint.vout).to.equal(1); }); - it('should allow to return object of lock', () => { + it('should flatten the inner proof shape next to the type tag', () => { const instantLockProof = new wasm.InstantAssetLockProof( instantLockBytes, transactionBytes, @@ -183,21 +180,20 @@ describe('AssetLockProof', () => { const instantAssetLockProof = new wasm.AssetLockProof(instantLockProof); - // InstantAssetLockProof.toObject() does not include type field - const expectedInner = { + const innerExpected = { instantLock: instantLockBytes, transaction: transactionBytes, outputIndex: 0, }; - // AssetLockProof.toObject() includes type field (0 = Instant) - const expectedOuter = { - ...expectedInner, - type: 0, + // AssetLockProof.toObject() flattens the inner fields next to `type` + const outerExpected = { + type: 'instant', + ...innerExpected, }; - expect(instantLockProof.toObject()).to.deep.equal(expectedInner); - expect(instantAssetLockProof.toObject()).to.deep.equal(expectedOuter); + expect(instantLockProof.toObject()).to.deep.equal(innerExpected); + expect(instantAssetLockProof.toObject()).to.deep.equal(outerExpected); }); }); @@ -210,8 +206,7 @@ describe('AssetLockProof', () => { ); const jsonRepresentation = instantLockProof.toJSON(); - // Should have type field (0 = Instant) - expect(jsonRepresentation.type).to.equal(0); + expect(jsonRepresentation.type).to.equal('instant'); expect(jsonRepresentation.instantLock).to.be.a('string'); expect(jsonRepresentation.transaction).to.be.a('string'); expect(Buffer.from(jsonRepresentation.instantLock, 'base64')).to.deep.equal( @@ -234,8 +229,7 @@ describe('AssetLockProof', () => { const chainLockProof = wasm.AssetLockProof.createChainAssetLockProof(1, outpoint); const jsonRepresentation = chainLockProof.toJSON(); - // Should have type field (1 = Chain) - expect(jsonRepresentation.type).to.equal(1); + expect(jsonRepresentation.type).to.equal('chain'); const restoredProof = wasm.AssetLockProof.fromJSON(jsonRepresentation); @@ -243,7 +237,7 @@ describe('AssetLockProof', () => { }); }); - describe('lockTypeName', () => { + describe('lockType', () => { it('should allow to get lock type', () => { const outpoint = new wasm.OutPoint( 'e8b43025641eea4fd21190f01bd870ef90f1a8b199d8fc3376c5b62c0b1a179d', @@ -258,8 +252,10 @@ describe('AssetLockProof', () => { const instantAssetLockProof = new wasm.AssetLockProof(instantLockProof); const chainLockProof = wasm.AssetLockProof.createChainAssetLockProof(1, outpoint); - expect(instantAssetLockProof.lockTypeName).to.equal('Instant'); - expect(chainLockProof.lockTypeName).to.equal('Chain'); + // lockType returns the lowercase wire-shape string, matching + // `AssetLockProof.toObject().type` for round-trip consistency. + expect(instantAssetLockProof.lockType).to.equal('instant'); + expect(chainLockProof.lockType).to.equal('chain'); }); }); diff --git a/packages/wasm-dpp2/tests/unit/FeeStrategyStep.spec.ts b/packages/wasm-dpp2/tests/unit/FeeStrategyStep.spec.ts new file mode 100644 index 00000000000..3976949f797 --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/FeeStrategyStep.spec.ts @@ -0,0 +1,109 @@ +import { expect } from './helpers/chai.ts'; +import { initWasm, wasm } from '../../dist/dpp.compressed.js'; + +before(async () => { + await initWasm(); +}); + +describe('FeeStrategyStep', () => { + describe('deductFromInput()', () => { + it('should create a deductFromInput step with the given index', () => { + const step = wasm.FeeStrategyStep.deductFromInput(2); + + expect(step).to.exist(); + expect(step).to.be.an.instanceof(wasm.FeeStrategyStep); + expect(step.isDeductFromInput).to.be.true(); + expect(step.isReduceOutput).to.be.false(); + expect(step.index).to.equal(2); + }); + + it('should accept index 0', () => { + const step = wasm.FeeStrategyStep.deductFromInput(0); + expect(step.index).to.equal(0); + }); + + it('should accept large indices up to u16::MAX', () => { + const step = wasm.FeeStrategyStep.deductFromInput(65535); + expect(step.index).to.equal(65535); + }); + }); + + describe('reduceOutput()', () => { + it('should create a reduceOutput step with the given index', () => { + const step = wasm.FeeStrategyStep.reduceOutput(7); + + expect(step.isReduceOutput).to.be.true(); + expect(step.isDeductFromInput).to.be.false(); + expect(step.index).to.equal(7); + }); + + it('should accept index 0', () => { + const step = wasm.FeeStrategyStep.reduceOutput(0); + expect(step.index).to.equal(0); + }); + }); + + describe('use as constructor argument to AddressFundsTransferTransition', () => { + const addrBytes = new Uint8Array([ + 0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + ]); + + function makeTransition(steps: any[]) { + const inputAddr = wasm.PlatformAddress.fromBytes(addrBytes); + const outputAddr = wasm.PlatformAddress.fromBytes(addrBytes); + const input = new wasm.PlatformAddressInput(inputAddr, 0, BigInt(100000)); + const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(90000)); + + return new wasm.AddressFundsTransferTransition({ + inputs: [input], + outputs: [output], + feeStrategy: steps, + }); + } + + it('emits {type: "deductFromInput", index} in toObject() output', () => { + const transition = makeTransition([wasm.FeeStrategyStep.deductFromInput(0)]); + const obj = transition.toObject(); + + expect(obj.feeStrategy).to.be.an('array').with.lengthOf(1); + expect(obj.feeStrategy[0]).to.deep.equal({ type: 'deductFromInput', index: 0 }); + }); + + it('emits {type: "reduceOutput", index} in toObject() output', () => { + const transition = makeTransition([wasm.FeeStrategyStep.reduceOutput(3)]); + const obj = transition.toObject(); + + expect(obj.feeStrategy).to.deep.equal([{ type: 'reduceOutput', index: 3 }]); + }); + + it('emits {type, index} in toJSON() output (matches Object form for this enum)', () => { + const transition = makeTransition([ + wasm.FeeStrategyStep.deductFromInput(1), + wasm.FeeStrategyStep.reduceOutput(2), + ]); + const json = transition.toJSON(); + + expect(json.feeStrategy).to.deep.equal([ + { type: 'deductFromInput', index: 1 }, + { type: 'reduceOutput', index: 2 }, + ]); + }); + + it('round-trips through fromObject(toObject())', () => { + const transition = makeTransition([ + wasm.FeeStrategyStep.deductFromInput(0), + wasm.FeeStrategyStep.reduceOutput(0), + ]); + const obj = transition.toObject(); + const restored = wasm.AddressFundsTransferTransition.fromObject(obj); + expect(restored.toObject().feeStrategy).to.deep.equal(obj.feeStrategy); + }); + + it('round-trips through fromJSON(toJSON())', () => { + const transition = makeTransition([wasm.FeeStrategyStep.reduceOutput(0)]); + const json = transition.toJSON(); + const restored = wasm.AddressFundsTransferTransition.fromJSON(json); + expect(restored.toJSON().feeStrategy).to.deep.equal(json.feeStrategy); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/IdentityCreateFromAddressesTransition.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityCreateFromAddressesTransition.spec.ts index 2de48feba78..1fcdf2cf592 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityCreateFromAddressesTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityCreateFromAddressesTransition.spec.ts @@ -171,6 +171,63 @@ describe('IdentityCreateFromAddressesTransition', () => { }); }); + describe('toObject() / toJSON() / fromObject() / fromJSON()', () => { + it('toObject() emits inputs as typed array of {address, nonce, amount}', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.inputs).to.be.an('array').with.lengthOf(1); + expect(obj.inputs[0].address).to.be.instanceOf(Uint8Array); + expect(obj.inputs[0].address.length).to.equal(21); + expect(obj.inputs[0].nonce).to.equal(0); + expect(obj.inputs[0].amount).to.equal(BigInt(100000)); + }); + + it('toObject() emits singular output as {address, amount}', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.output).to.be.an('object'); + expect(obj.output.address).to.be.instanceOf(Uint8Array); + expect(obj.output.address.length).to.equal(21); + expect(obj.output.amount).to.equal(BigInt(90000)); + }); + + it('toObject() emits feeStrategy with {type, index} shape', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.feeStrategy).to.be.an('array'); + expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].index).to.be.a('number'); + }); + + it('toJSON() emits hex addresses and number/string amounts', () => { + const transition = createTransition(); + const json = transition.toJSON(); + + expect(json.inputs[0].address).to.be.a('string').with.lengthOf(42); + expect(json.inputs[0].nonce).to.equal(0); + expect(json.output).to.be.an('object'); + expect(json.output.address).to.be.a('string').with.lengthOf(42); + expect(json.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + }); + + it('fromObject(toObject()) round-trips identically', () => { + const transition = createTransition(); + const obj = transition.toObject(); + const restored = wasm.IdentityCreateFromAddressesTransition.fromObject(obj); + expect(restored.toObject()).to.deep.equal(obj); + }); + + it('fromJSON(toJSON()) round-trips identically', () => { + const transition = createTransition(); + const json = transition.toJSON(); + const restored = wasm.IdentityCreateFromAddressesTransition.fromJSON(json); + expect(restored.toJSON()).to.deep.equal(json); + }); + }); + describe('toStateTransition() / fromStateTransition()', () => { it('should convert to and from StateTransition', () => { const transition = createTransition(); diff --git a/packages/wasm-dpp2/tests/unit/IdentityCreateTransition.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityCreateTransition.spec.ts index 4865e1412eb..0d7953aeb67 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityCreateTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityCreateTransition.spec.ts @@ -55,7 +55,9 @@ describe('IdentityCreateTransition', () => { expect(json.$formatVersion).to.equal('0'); expect(json.publicKeys).to.be.an('array'); expect(json.publicKeys.length).to.equal(0); + // AssetLockProof emits internally-tagged shape: { type, ...inner fields } expect(json.assetLockProof).to.be.an('object'); + expect(json.assetLockProof.type).to.equal('instant'); expect(json.assetLockProof.outputIndex).to.equal(0); expect(json.userFeeIncrease).to.equal(0); expect(json.signature).to.equal(''); diff --git a/packages/wasm-dpp2/tests/unit/IdentityCreditTransferToAddresses.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityCreditTransferToAddresses.spec.ts index fad144990f2..a412610ed1c 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityCreditTransferToAddresses.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityCreditTransferToAddresses.spec.ts @@ -183,6 +183,41 @@ describe('IdentityCreditTransferToAddresses', () => { }); }); + describe('toObject() / toJSON() / fromObject() / fromJSON()', () => { + it('toObject() emits recipientAddresses as typed array of {address, amount}', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.recipientAddresses).to.be.an('array').with.lengthOf(1); + expect(obj.recipientAddresses[0].address).to.be.instanceOf(Uint8Array); + expect(obj.recipientAddresses[0].address.length).to.equal(21); + expect(obj.recipientAddresses[0].amount).to.equal(BigInt(90000)); + }); + + it('toJSON() emits recipientAddresses with hex addresses and number/string amounts', () => { + const transition = createTransition(); + const json = transition.toJSON(); + + expect(json.recipientAddresses).to.be.an('array').with.lengthOf(1); + expect(json.recipientAddresses[0].address).to.be.a('string').with.lengthOf(42); + expect(json.recipientAddresses[0].amount).to.satisfy((v: unknown) => typeof v === 'number' || typeof v === 'string'); + }); + + it('fromObject(toObject()) round-trips identically', () => { + const transition = createTransition(); + const obj = transition.toObject(); + const restored = wasm.IdentityCreditTransferToAddresses.fromObject(obj); + expect(restored.toObject()).to.deep.equal(obj); + }); + + it('fromJSON(toJSON()) round-trips identically', () => { + const transition = createTransition(); + const json = transition.toJSON(); + const restored = wasm.IdentityCreditTransferToAddresses.fromJSON(json); + expect(restored.toJSON()).to.deep.equal(json); + }); + }); + describe('toStateTransition() / fromStateTransition()', () => { it('should convert to and from StateTransition', () => { const transition = createTransition(); diff --git a/packages/wasm-dpp2/tests/unit/IdentityCreditWithdrawalTransition.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityCreditWithdrawalTransition.spec.ts index 16941238d28..7f2433f71d0 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityCreditWithdrawalTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityCreditWithdrawalTransition.spec.ts @@ -424,7 +424,7 @@ describe('IdentityCreditWithdrawalTransition', () => { expect(json.identityId).to.equal('GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec'); expect(json.amount).to.equal(111); expect(json.coreFeePerByte).to.equal(1); - expect(json.pooling).to.equal(0); + expect(json.pooling).to.equal('never'); expect(json.outputScript).to.equal('dqkUAQEBAQEBAQEBAQEBAQEBAQEBAQGIrA=='); expect(json.nonce).to.equal(1); expect(json.userFeeIncrease).to.equal(1); diff --git a/packages/wasm-dpp2/tests/unit/IdentityTopUpFromAddressesTransition.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityTopUpFromAddressesTransition.spec.ts index 40cb18eab1f..1a925ef8af2 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityTopUpFromAddressesTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityTopUpFromAddressesTransition.spec.ts @@ -164,6 +164,62 @@ describe('IdentityTopUpFromAddressesTransition', () => { }); }); + describe('toObject() / toJSON() / fromObject() / fromJSON()', () => { + it('toObject() emits inputs as typed array of {address, nonce, amount}', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.inputs).to.be.an('array').with.lengthOf(1); + expect(obj.inputs[0].address).to.be.instanceOf(Uint8Array); + expect(obj.inputs[0].address.length).to.equal(21); + expect(obj.inputs[0].nonce).to.equal(0); + expect(obj.inputs[0].amount).to.equal(BigInt(100000)); + }); + + it('toObject() emits singular output as {address, amount}', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.output).to.exist(); + expect(obj.output.address).to.be.instanceOf(Uint8Array); + expect(obj.output.address.length).to.equal(21); + expect(obj.output.amount).to.equal(BigInt(90000)); + }); + + it('toObject() emits feeStrategy with {type, index} shape', () => { + const transition = createTransition(); + const obj = transition.toObject(); + + expect(obj.feeStrategy).to.be.an('array'); + expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].index).to.be.a('number'); + }); + + it('toJSON() emits hex addresses and number/string amounts', () => { + const transition = createTransition(); + const json = transition.toJSON(); + + expect(json.inputs[0].address).to.be.a('string').with.lengthOf(42); + expect(json.output).to.exist(); + expect(json.output.address).to.be.a('string').with.lengthOf(42); + expect(json.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + }); + + it('fromObject(toObject()) round-trips identically', () => { + const transition = createTransition(); + const obj = transition.toObject(); + const restored = wasm.IdentityTopUpFromAddressesTransition.fromObject(obj); + expect(restored.toObject()).to.deep.equal(obj); + }); + + it('fromJSON(toJSON()) round-trips identically', () => { + const transition = createTransition(); + const json = transition.toJSON(); + const restored = wasm.IdentityTopUpFromAddressesTransition.fromJSON(json); + expect(restored.toJSON()).to.deep.equal(json); + }); + }); + describe('toStateTransition() / fromStateTransition()', () => { it('should convert to and from StateTransition', () => { const transition = createTransition(); diff --git a/packages/wasm-dpp2/tests/unit/IdentityTopUpTransition.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityTopUpTransition.spec.ts index 0022e46ba1b..818d6bdec12 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityTopUpTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityTopUpTransition.spec.ts @@ -162,7 +162,9 @@ describe('IdentityTopUpTransition', () => { expect(json.$formatVersion).to.equal('0'); expect(json.identityId).to.equal(testIdentityId); + // AssetLockProof emits internally-tagged shape: { type, ...inner fields } expect(json.assetLockProof).to.be.an('object'); + expect(json.assetLockProof.type).to.equal('instant'); expect(json.assetLockProof).to.have.property('instantLock'); expect(json.assetLockProof).to.have.property('transaction'); expect(json.assetLockProof.outputIndex).to.equal(0); diff --git a/packages/wasm-dpp2/tests/unit/ProofResult.spec.ts b/packages/wasm-dpp2/tests/unit/ProofResult.spec.ts index b6f69a698f1..bcd33cceda6 100644 --- a/packages/wasm-dpp2/tests/unit/ProofResult.spec.ts +++ b/packages/wasm-dpp2/tests/unit/ProofResult.spec.ts @@ -391,6 +391,24 @@ describe('StateTransitionProofResult types', () => { expect(result.balances.size).to.equal(0); }); + + // Regression: toJSON() must produce a value where JSON.stringify preserves + // Map entries. js_sys::Map embedded in a JsValue serialises to {} via + // JSON.stringify, so toJSON() has to normalise the Map to a plain object. + it('toJSON() should preserve Map entries through JSON.stringify', () => { + const id1 = new wasm.Identifier(identifier); + const balancesMap = new Map(); + balancesMap.set(id1.toBase58(), 999000n); + + const result = wasm.VerifiedTokenIdentitiesBalances.fromObject({ balances: balancesMap }); + const json = result.toJSON(); + + const stringified = JSON.stringify(json); + const parsed = JSON.parse(stringified); + expect(parsed.balances).to.have.property(id1.toBase58()); + // BigInt is normalised to a string for JSON compatibility. + expect(parsed.balances[id1.toBase58()]).to.equal('999000'); + }); }); describe('VerifiedDocuments', () => { @@ -419,6 +437,18 @@ describe('StateTransitionProofResult types', () => { expect(result.documents.size).to.equal(0); }); + + it('toJSON() should preserve Map entries through JSON.stringify', () => { + const id1 = new wasm.Identifier(identifier); + const docsMap = new Map(); + docsMap.set(id1.toBase58(), null); + + const result = wasm.VerifiedDocuments.fromObject({ documents: docsMap }); + const parsed = JSON.parse(JSON.stringify(result.toJSON())); + + expect(parsed.documents).to.have.property(id1.toBase58()); + expect(parsed.documents[id1.toBase58()]).to.equal(null); + }); }); describe('VerifiedAddressInfos', () => { @@ -446,6 +476,17 @@ describe('StateTransitionProofResult types', () => { expect(result.addressInfos.size).to.equal(0); }); + + it('toJSON() should preserve Map entries through JSON.stringify', () => { + const infosMap = new Map(); + infosMap.set('abcdef0123456789', null); + + const result = wasm.VerifiedAddressInfos.fromObject({ addressInfos: infosMap }); + const parsed = JSON.parse(JSON.stringify(result.toJSON())); + + expect(parsed.addressInfos).to.have.property('abcdef0123456789'); + expect(parsed.addressInfos.abcdef0123456789).to.equal(null); + }); }); // ========================================================================= @@ -476,6 +517,27 @@ describe('StateTransitionProofResult types', () => { expect(obj).to.have.property('identity'); expect(obj).to.have.property('addressInfos'); }); + + it('toJSON() should preserve Map entries through JSON.stringify', () => { + const identityData = { + $formatVersion: '0', + id: identifier, + publicKeys: [], + balance: 0, + revision: 0, + }; + const infosMap = new Map(); + infosMap.set('deadbeef', null); + + const result = wasm.VerifiedIdentityFullWithAddressInfos.fromObject({ + identity: identityData, + addressInfos: infosMap, + }); + const parsed = JSON.parse(JSON.stringify(result.toJSON())); + + expect(parsed.addressInfos).to.have.property('deadbeef'); + expect(parsed.addressInfos.deadbeef).to.equal(null); + }); }); describe('VerifiedIdentityWithAddressInfos', () => { @@ -503,5 +565,27 @@ describe('StateTransitionProofResult types', () => { expect(obj).to.have.property('partialIdentity'); expect(obj).to.have.property('addressInfos'); }); + + it('toJSON() should preserve Map entries through JSON.stringify', () => { + const idBytes = new wasm.Identifier(identifier).toBytes(); + const piData = { + id: idBytes, + loadedPublicKeys: {}, + balance: null, + revision: null, + notFoundPublicKeys: [], + }; + const infosMap = new Map(); + infosMap.set('cafebabe', null); + + const result = wasm.VerifiedIdentityWithAddressInfos.fromObject({ + partialIdentity: piData, + addressInfos: infosMap, + }); + const parsed = JSON.parse(JSON.stringify(result.toJSON())); + + expect(parsed.addressInfos).to.have.property('cafebabe'); + expect(parsed.addressInfos.cafebabe).to.equal(null); + }); }); }); diff --git a/packages/wasm-dpp2/tests/unit/ShieldFromAssetLockTransition.spec.ts b/packages/wasm-dpp2/tests/unit/ShieldFromAssetLockTransition.spec.ts new file mode 100644 index 00000000000..f1ed5196b5a --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/ShieldFromAssetLockTransition.spec.ts @@ -0,0 +1,102 @@ +import { expect } from './helpers/chai.ts'; +import { initWasm, wasm } from '../../dist/dpp.compressed.js'; +import { + fakeOrchardAction, + ZERO_ANCHOR, + ZERO_BINDING_SIG, + ZERO_PROOF, +} from './helpers/shielded.ts'; +import { instantLockBytes, transactionBytes } from './mocks/Locks/index.js'; + +before(async () => { + await initWasm(); +}); + +describe('ShieldFromAssetLockTransition', () => { + function createAssetLockProof() { + return wasm.AssetLockProof.createInstantAssetLockProof( + instantLockBytes, + transactionBytes, + 0, + ); + } + + function createTransition() { + return new wasm.ShieldFromAssetLockTransition({ + assetLockProof: createAssetLockProof(), + actions: [fakeOrchardAction()], + valueBalance: BigInt(0), + anchor: ZERO_ANCHOR, + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + signature: new Uint8Array(0), + }); + } + + describe('constructor()', () => { + it('should construct with assetLockProof + Orchard fields', () => { + const t = createTransition(); + expect(t).to.be.an.instanceof(wasm.ShieldFromAssetLockTransition); + }); + + it('should reject missing assetLockProof', () => { + expect(() => new wasm.ShieldFromAssetLockTransition({ + actions: [fakeOrchardAction()], + valueBalance: BigInt(0), + anchor: ZERO_ANCHOR, + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + signature: new Uint8Array(0), + })).to.throw(); + }); + }); + + describe('getters', () => { + it('returns AssetLockProof and typed actions', () => { + const t = createTransition(); + expect(t.assetLockProof).to.be.an.instanceof(wasm.AssetLockProof); + expect(t.assetLockProof.lockType).to.equal('instant'); + expect(t.actions).to.be.an('array').with.lengthOf(1); + expect(t.actions[0]).to.be.an.instanceof(wasm.SerializedOrchardAction); + }); + }); + + describe('toBytes() / fromBytes()', () => { + it('round-trips via bytes', () => { + const t = createTransition(); + const bytes = t.toBytes(); + const restored = wasm.ShieldFromAssetLockTransition.fromBytes(bytes); + expect(Buffer.from(restored.toBytes())).to.deep.equal(Buffer.from(bytes)); + }); + }); + + describe('toObject() / toJSON()', () => { + it('toObject() emits AssetLockProof in internally-tagged Object form', () => { + const t = createTransition(); + const obj = t.toObject(); + // Internally-tagged: { type: "instant" | "chain", ...flattened inner fields } + expect(obj.assetLockProof).to.be.an('object'); + expect(obj.assetLockProof.type).to.be.oneOf(['instant', 'chain']); + expect(obj.actions).to.be.an('array').with.lengthOf(1); + expect(obj.anchor).to.be.instanceOf(Uint8Array).with.lengthOf(32); + expect(obj.bindingSignature).to.be.instanceOf(Uint8Array).with.lengthOf(64); + }); + + it('toJSON() emits AssetLockProof + byte fields with the JSON shape', () => { + const t = createTransition(); + const json = t.toJSON(); + expect(json.assetLockProof).to.be.an('object'); + expect(json.assetLockProof.type).to.be.oneOf(['instant', 'chain']); + expect(json.actions).to.be.an('array').with.lengthOf(1); + expect(json.anchor).to.be.a('string').with.lengthOf(44); // 32 bytes base64 + expect(json.bindingSignature).to.be.a('string').with.lengthOf(88); // 64 bytes base64 + }); + }); + + describe('toStateTransition()', () => { + it('should convert to StateTransition wrapper', () => { + const t = createTransition(); + expect(t.toStateTransition()).to.exist(); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/ShieldTransition.spec.ts b/packages/wasm-dpp2/tests/unit/ShieldTransition.spec.ts new file mode 100644 index 00000000000..33a3e228f3e --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/ShieldTransition.spec.ts @@ -0,0 +1,113 @@ +import { expect } from './helpers/chai.ts'; +import { initWasm, wasm } from '../../dist/dpp.compressed.js'; +import { + fakeOrchardAction, + ZERO_ANCHOR, + ZERO_BINDING_SIG, + ZERO_PROOF, +} from './helpers/shielded.ts'; + +before(async () => { + await initWasm(); +}); + +describe('ShieldTransition', () => { + const addrBytes = new Uint8Array([ + 0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + ]); + + function createTransition() { + const inputAddr = wasm.PlatformAddress.fromBytes(addrBytes); + const input = new wasm.PlatformAddressInput(inputAddr, 0, BigInt(100_000)); + const witness = wasm.AddressWitness.p2pkh(new Uint8Array(65)); + + return new wasm.ShieldTransition({ + inputs: [input], + actions: [fakeOrchardAction()], + amount: BigInt(50_000), + anchor: ZERO_ANCHOR, + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + inputWitnesses: [witness], + }); + } + + describe('constructor()', () => { + it('should construct with required fields', () => { + const t = createTransition(); + expect(t).to.be.an.instanceof(wasm.ShieldTransition); + }); + + it('should reject anchor of wrong length', () => { + expect(() => new wasm.ShieldTransition({ + inputs: [], + actions: [fakeOrchardAction()], + amount: BigInt(0), + anchor: new Uint8Array(31), + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + inputWitnesses: [], + })).to.throw(); + }); + }); + + describe('getters', () => { + it('returns typed inputs / actions / inputWitnesses / feeStrategy', () => { + const t = createTransition(); + expect(t.inputs).to.be.an('array').with.lengthOf(1); + expect(t.inputs[0]).to.be.an.instanceof(wasm.PlatformAddressInput); + expect(t.actions[0]).to.be.an.instanceof(wasm.SerializedOrchardAction); + expect(t.inputWitnesses[0]).to.be.an.instanceof(wasm.AddressWitness); + expect(t.feeStrategy).to.be.an('array'); + expect(t.amount).to.equal(BigInt(50_000)); + }); + }); + + describe('toBytes() / fromBytes()', () => { + it('round-trips via bytes', () => { + const t = createTransition(); + const bytes = t.toBytes(); + const restored = wasm.ShieldTransition.fromBytes(bytes); + expect(Buffer.from(restored.toBytes())).to.deep.equal(Buffer.from(bytes)); + }); + }); + + describe('toObject() / toJSON()', () => { + it('toObject() emits inputs as typed array of {address, nonce, amount}', () => { + const t = createTransition(); + const obj = t.toObject(); + + expect(obj.inputs).to.be.an('array').with.lengthOf(1); + expect(obj.inputs[0].address).to.be.instanceOf(Uint8Array); + expect(obj.inputs[0].address.length).to.equal(21); + expect(obj.inputs[0].nonce).to.equal(0); + expect(obj.inputs[0].amount).to.equal(BigInt(100_000)); + }); + + it('toObject() emits feeStrategy with {type, index} shape', () => { + const t = createTransition(); + const obj = t.toObject(); + expect(obj.feeStrategy).to.be.an('array'); + if (obj.feeStrategy.length > 0) { + expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].index).to.be.a('number'); + } + }); + + it('toJSON() emits inputs with hex address and number/string amount', () => { + const t = createTransition(); + const json = t.toJSON(); + + expect(json.inputs[0].address).to.be.a('string').with.lengthOf(42); + expect(json.inputs[0].nonce).to.equal(0); + expect(json.inputs[0].amount).to.satisfy((v: unknown) => typeof v === 'number' || typeof v === 'string'); + }); + }); + + describe('toStateTransition()', () => { + it('should convert to StateTransition wrapper', () => { + const t = createTransition(); + expect(t.toStateTransition()).to.exist(); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/ShieldedTransferTransition.spec.ts b/packages/wasm-dpp2/tests/unit/ShieldedTransferTransition.spec.ts new file mode 100644 index 00000000000..891da92605d --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/ShieldedTransferTransition.spec.ts @@ -0,0 +1,134 @@ +import { expect } from './helpers/chai.ts'; +import { initWasm, wasm } from '../../dist/dpp.compressed.js'; +import { + fakeOrchardAction, + ZERO_ANCHOR, + ZERO_BINDING_SIG, + ZERO_PROOF, +} from './helpers/shielded.ts'; + +before(async () => { + await initWasm(); +}); + +describe('ShieldedTransferTransition', () => { + function createTransition() { + return new wasm.ShieldedTransferTransition({ + actions: [fakeOrchardAction()], + valueBalance: BigInt(0), + anchor: ZERO_ANCHOR, + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + }); + } + + describe('constructor()', () => { + it('should construct with valid Orchard fields', () => { + const t = createTransition(); + expect(t).to.be.an.instanceof(wasm.ShieldedTransferTransition); + }); + + it('should reject anchor of wrong length', () => { + expect(() => new wasm.ShieldedTransferTransition({ + actions: [fakeOrchardAction()], + valueBalance: BigInt(0), + anchor: new Uint8Array(31), // too short + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + })).to.throw(); + }); + + it('should reject bindingSignature of wrong length', () => { + expect(() => new wasm.ShieldedTransferTransition({ + actions: [fakeOrchardAction()], + valueBalance: BigInt(0), + anchor: ZERO_ANCHOR, + proof: ZERO_PROOF, + bindingSignature: new Uint8Array(63), + })).to.throw(); + }); + }); + + describe('getters', () => { + it('should expose actions, anchor, proof, bindingSignature, valueBalance', () => { + const t = createTransition(); + expect(t.actions).to.be.an('array').with.lengthOf(1); + expect(t.actions[0]).to.be.an.instanceof(wasm.SerializedOrchardAction); + expect(t.anchor).to.be.instanceOf(Uint8Array); + expect(t.anchor.length).to.equal(32); + expect(t.bindingSignature.length).to.equal(64); + expect(t.valueBalance).to.equal(BigInt(0)); + }); + }); + + describe('toBytes() / fromBytes()', () => { + it('should round-trip via bytes', () => { + const t = createTransition(); + const bytes = t.toBytes(); + const restored = wasm.ShieldedTransferTransition.fromBytes(bytes); + expect(Buffer.from(restored.toBytes())).to.deep.equal(Buffer.from(bytes)); + }); + }); + + describe('toObject() / toJSON()', () => { + it('toObject() emits typed actions array with Uint8Array byte fields', () => { + const t = createTransition(); + const obj = t.toObject(); + + expect(obj.actions).to.be.an('array').with.lengthOf(1); + const a = obj.actions[0]; + expect(a.nullifier).to.be.instanceOf(Uint8Array); + expect(a.nullifier.length).to.equal(32); + expect(a.rk).to.be.instanceOf(Uint8Array); + expect(a.rk.length).to.equal(32); + expect(a.cmx).to.be.instanceOf(Uint8Array); + expect(a.cmx.length).to.equal(32); + expect(a.encryptedNote).to.be.instanceOf(Uint8Array); + expect(a.cvNet).to.be.instanceOf(Uint8Array); + expect(a.cvNet.length).to.equal(32); + expect(a.spendAuthSig).to.be.instanceOf(Uint8Array); + expect(a.spendAuthSig.length).to.equal(64); + }); + + it('toJSON() emits action byte fields as base64 strings', () => { + const t = createTransition(); + const json = t.toJSON(); + + const a = json.actions[0]; + expect(a.nullifier).to.be.a('string').with.lengthOf(44); // 32 bytes base64 + expect(a.rk).to.be.a('string').with.lengthOf(44); + expect(a.cmx).to.be.a('string').with.lengthOf(44); + expect(a.encryptedNote).to.be.a('string'); + expect(a.cvNet).to.be.a('string').with.lengthOf(44); + expect(a.spendAuthSig).to.be.a('string').with.lengthOf(88); // 64 bytes base64 + }); + + it('toObject() emits anchor / proof / bindingSignature as Uint8Array', () => { + const t = createTransition(); + const obj = t.toObject(); + + expect(obj.anchor).to.be.instanceOf(Uint8Array); + expect(obj.anchor.length).to.equal(32); + expect(obj.proof).to.be.instanceOf(Uint8Array); + expect(obj.bindingSignature).to.be.instanceOf(Uint8Array); + expect(obj.bindingSignature.length).to.equal(64); + }); + + it('toJSON() emits anchor / proof / bindingSignature as base64 strings', () => { + const t = createTransition(); + const json = t.toJSON(); + + expect(json.anchor).to.be.a('string').with.lengthOf(44); // 32 bytes base64 + expect(json.proof).to.be.a('string'); + expect(json.bindingSignature).to.be.a('string').with.lengthOf(88); // 64 bytes base64 + }); + }); + + describe('toStateTransition()', () => { + it('should convert to StateTransition wrapper', () => { + const t = createTransition(); + const st = t.toStateTransition(); + expect(st).to.exist(); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/ShieldedWithdrawalTransition.spec.ts b/packages/wasm-dpp2/tests/unit/ShieldedWithdrawalTransition.spec.ts new file mode 100644 index 00000000000..0ec4cb470da --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/ShieldedWithdrawalTransition.spec.ts @@ -0,0 +1,89 @@ +import { expect } from './helpers/chai.ts'; +import { initWasm, wasm } from '../../dist/dpp.compressed.js'; +import { + fakeOrchardAction, + ZERO_ANCHOR, + ZERO_BINDING_SIG, + ZERO_PROOF, +} from './helpers/shielded.ts'; + +before(async () => { + await initWasm(); +}); + +describe('ShieldedWithdrawalTransition', () => { + function createTransition(pooling: any = 'never') { + return new wasm.ShieldedWithdrawalTransition({ + actions: [fakeOrchardAction()], + unshieldingAmount: BigInt(50_000), + anchor: ZERO_ANCHOR, + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + coreFeePerByte: 1, + pooling, + outputScript: new Uint8Array(25), + }); + } + + describe('constructor() + pooling polymorphic input (matches IdentityCreditWithdrawal)', () => { + it('accepts pooling as a name string', () => { + const t = createTransition('never'); + // PoolingWasm::From<...> for String returns CamelCase variant names. + expect(t.pooling.toLowerCase()).to.equal('never'); + }); + + it('accepts pooling as a numeric value', () => { + const t = createTransition(1); + expect(t.pooling.toLowerCase()).to.equal('ifavailable'); + }); + + it('accepts pooling as a Pooling enum value', () => { + // wasm-bindgen exports the enum as `PoolingWasm` (Never=0, IfAvailable=1, Standard=2). + const t = createTransition(wasm.PoolingWasm.Standard); + expect(t.pooling.toLowerCase()).to.equal('standard'); + }); + + it('rejects an unknown pooling string', () => { + expect(() => createTransition('nonsense')).to.throw(); + }); + }); + + describe('toBytes() / fromBytes()', () => { + it('round-trips via bytes', () => { + const t = createTransition('never'); + const bytes = t.toBytes(); + const restored = wasm.ShieldedWithdrawalTransition.fromBytes(bytes); + expect(Buffer.from(restored.toBytes())).to.deep.equal(Buffer.from(bytes)); + }); + }); + + describe('toObject() / toJSON()', () => { + it('toObject() emits pooling as the Pooling enum (matches IdentityCreditWithdrawal)', () => { + const t = createTransition('ifavailable'); + const obj = t.toObject(); + + // dpp::withdrawal::Pooling derives serde_repr → emits as a u8 in + // non-human-readable formats (which is what platform_value uses). + expect(obj.pooling).to.satisfy( + (v: unknown) => typeof v === 'number' || typeof v === 'string', + ); + }); + + it('toJSON() emits pooling as a number (Pooling has serde_repr)', () => { + const t = createTransition('standard'); + const json = t.toJSON(); + + // serde_repr also stringifies to a number in human-readable mode. + expect(json.pooling).to.satisfy( + (v: unknown) => typeof v === 'number' || typeof v === 'string', + ); + }); + }); + + describe('toStateTransition()', () => { + it('should convert to StateTransition wrapper', () => { + const t = createTransition(); + expect(t.toStateTransition()).to.exist(); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/UnshieldTransition.spec.ts b/packages/wasm-dpp2/tests/unit/UnshieldTransition.spec.ts new file mode 100644 index 00000000000..0b9325ced9c --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/UnshieldTransition.spec.ts @@ -0,0 +1,98 @@ +import { expect } from './helpers/chai.ts'; +import { initWasm, wasm } from '../../dist/dpp.compressed.js'; +import { + fakeOrchardAction, + ZERO_ANCHOR, + ZERO_BINDING_SIG, + ZERO_PROOF, +} from './helpers/shielded.ts'; + +before(async () => { + await initWasm(); +}); + +describe('UnshieldTransition', () => { + const addrBytes = new Uint8Array([ + 0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + ]); + + function createTransition() { + const outputAddr = wasm.PlatformAddress.fromBytes(addrBytes); + + return new wasm.UnshieldTransition({ + outputAddress: outputAddr, + actions: [fakeOrchardAction()], + unshieldingAmount: BigInt(50_000), + anchor: ZERO_ANCHOR, + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + }); + } + + describe('constructor()', () => { + it('should construct with outputAddress + Orchard fields', () => { + const t = createTransition(); + expect(t).to.be.an.instanceof(wasm.UnshieldTransition); + }); + + it('should reject missing outputAddress', () => { + expect(() => new wasm.UnshieldTransition({ + actions: [fakeOrchardAction()], + unshieldingAmount: BigInt(0), + anchor: ZERO_ANCHOR, + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + })).to.throw(); + }); + }); + + describe('getters', () => { + it('returns typed PlatformAddress for outputAddress', () => { + const t = createTransition(); + const addr = t.outputAddress; + expect(addr).to.be.an.instanceof(wasm.PlatformAddress); + expect(addr.toBytes()).to.deep.equal(addrBytes); + }); + + it('returns typed Orchard actions', () => { + const t = createTransition(); + expect(t.actions[0]).to.be.an.instanceof(wasm.SerializedOrchardAction); + }); + + it('returns the unshielding amount', () => { + const t = createTransition(); + expect(t.unshieldingAmount).to.equal(BigInt(50_000)); + }); + }); + + describe('toBytes() / fromBytes()', () => { + it('round-trips via bytes', () => { + const t = createTransition(); + const bytes = t.toBytes(); + const restored = wasm.UnshieldTransition.fromBytes(bytes); + expect(Buffer.from(restored.toBytes())).to.deep.equal(Buffer.from(bytes)); + }); + }); + + describe('toObject() / toJSON()', () => { + it('toObject() emits outputAddress as a 21-byte Uint8Array', () => { + const t = createTransition(); + const obj = t.toObject(); + expect(obj.outputAddress).to.be.instanceOf(Uint8Array); + expect(obj.outputAddress.length).to.equal(21); + }); + + it('toJSON() emits outputAddress as a 42-char hex string', () => { + const t = createTransition(); + const json = t.toJSON(); + expect(json.outputAddress).to.be.a('string').with.lengthOf(42); // 21 bytes hex + }); + }); + + describe('toStateTransition()', () => { + it('should convert to StateTransition wrapper', () => { + const t = createTransition(); + expect(t.toStateTransition()).to.exist(); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/VerifiedShieldedNullifiers.spec.ts b/packages/wasm-dpp2/tests/unit/VerifiedShieldedNullifiers.spec.ts new file mode 100644 index 00000000000..72b8a06d49e --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/VerifiedShieldedNullifiers.spec.ts @@ -0,0 +1,103 @@ +import { expect } from './helpers/chai.ts'; +import { initWasm, wasm } from '../../dist/dpp.compressed.js'; + +before(async () => { + await initWasm(); +}); + +// These wrappers wrap js_sys::Map under the hood. Without normalisation, +// JSON.stringify silently drops Map entries (Map has no enumerable own +// properties), so the toJSON() tests below would have caught the regression +// fixed in proof_result_shielded.rs. + +describe('VerifiedShieldedNullifiers', () => { + it('fromObject/toObject preserves Map entries', () => { + const nullifiers = new Map(); + nullifiers.set('deadbeef', true); + + const result = wasm.VerifiedShieldedNullifiers.fromObject({ nullifiers }); + expect(result.nullifiers).to.be.instanceOf(Map); + expect(result.nullifiers.size).to.equal(1); + + const obj = result.toObject(); + expect(obj.nullifiers).to.be.instanceOf(Map); + }); + + it('toJSON() preserves Map entries through JSON.stringify', () => { + const nullifiers = new Map(); + nullifiers.set('deadbeef', true); + nullifiers.set('cafebabe', false); + + const result = wasm.VerifiedShieldedNullifiers.fromObject({ nullifiers }); + const parsed = JSON.parse(JSON.stringify(result.toJSON())); + + expect(parsed.nullifiers).to.have.property('deadbeef', true); + expect(parsed.nullifiers).to.have.property('cafebabe', false); + }); +}); + +describe('VerifiedShieldedNullifiersWithAddressInfos', () => { + it('fromObject/toObject preserves Map entries', () => { + const nullifiers = new Map(); + nullifiers.set('aa', true); + const addressInfos = new Map(); + addressInfos.set('bb', null); + + const result = wasm.VerifiedShieldedNullifiersWithAddressInfos.fromObject({ + nullifiers, + addressInfos, + }); + expect(result.nullifiers.size).to.equal(1); + expect(result.addressInfos.size).to.equal(1); + }); + + it('toJSON() preserves Map entries through JSON.stringify', () => { + const nullifiers = new Map(); + nullifiers.set('aa', true); + const addressInfos = new Map(); + addressInfos.set('bb', null); + + const result = wasm.VerifiedShieldedNullifiersWithAddressInfos.fromObject({ + nullifiers, + addressInfos, + }); + const parsed = JSON.parse(JSON.stringify(result.toJSON())); + + expect(parsed.nullifiers).to.have.property('aa', true); + expect(parsed.addressInfos).to.have.property('bb'); + expect(parsed.addressInfos.bb).to.equal(null); + }); +}); + +describe('VerifiedShieldedNullifiersWithWithdrawalDocument', () => { + it('fromObject/toObject preserves Map entries', () => { + const nullifiers = new Map(); + nullifiers.set('aa', true); + const documents = new Map(); + documents.set('bb', null); + + const result = wasm.VerifiedShieldedNullifiersWithWithdrawalDocument.fromObject({ + nullifiers, + documents, + }); + expect(result.nullifiers.size).to.equal(1); + expect(result.documents.size).to.equal(1); + }); + + it('toJSON() preserves Map entries through JSON.stringify', () => { + const nullifiers = new Map(); + nullifiers.set('aa', true); + const documents = new Map(); + documents.set('bb', null); + + const result = wasm.VerifiedShieldedNullifiersWithWithdrawalDocument.fromObject({ + nullifiers, + documents, + }); + const parsed = JSON.parse(JSON.stringify(result.toJSON())); + + expect(parsed.nullifiers).to.have.property('aa', true); + expect(parsed.documents).to.have.property('bb'); + expect(parsed.documents.bb).to.equal(null); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/helpers/shielded.ts b/packages/wasm-dpp2/tests/unit/helpers/shielded.ts new file mode 100644 index 00000000000..f21b53d7d87 --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/helpers/shielded.ts @@ -0,0 +1,20 @@ +import { wasm } from '../../../dist/dpp.compressed.js'; + +/// Build a placeholder `SerializedOrchardAction` filled with deterministic +/// non-zero bytes. The cryptographic content is not verified at the wasm-dpp2 +/// layer (that happens later in consensus validation), so wrappers accept any +/// well-shaped bytes for shape-level tests. +export function fakeOrchardAction(seed = 1): any { + return new wasm.SerializedOrchardAction({ + nullifier: new Uint8Array(32).fill(seed), + rk: new Uint8Array(32).fill(seed + 1), + cmx: new Uint8Array(32).fill(seed + 2), + encryptedNote: new Uint8Array(216).fill(seed + 3), + cvNet: new Uint8Array(32).fill(seed + 4), + spendAuthSig: new Uint8Array(64).fill(seed + 5), + }); +} + +export const ZERO_ANCHOR = new Uint8Array(32); +export const ZERO_PROOF = new Uint8Array(256); // arbitrary placeholder size +export const ZERO_BINDING_SIG = new Uint8Array(64); 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..57355087bdd --- /dev/null +++ b/packages/wasm-sdk/src/queries/shielded.rs @@ -0,0 +1,321 @@ +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; + +// ── Wrapper types ────────────────────────────────────────────────────── + +#[dpp_json_convertible_derive::json_safe_fields(crate = "dash_sdk::dpp")] +#[wasm_bindgen(js_name = "ShieldedEncryptedNote")] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ShieldedEncryptedNoteWasm { + cmx: Vec, + nullifier: Vec, + 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); + +#[dpp_json_convertible_derive::json_safe_fields(crate = "dash_sdk::dpp")] +#[wasm_bindgen(js_name = "ShieldedNullifierStatus")] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ShieldedNullifierStatusWasm { + 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); + +// ── 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")] + pub async fn get_shielded_pool_state(&self) -> Result, WasmSdkError> { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{NoParamQuery, ShieldedPoolState}; + + let result = ShieldedPoolState::fetch(self.as_ref(), NoParamQuery {}).await?; + Ok(result.map(|s| s.0)) + } + + /// Fetches encrypted notes from the shielded pool, paginated. + #[wasm_bindgen( + js_name = "getShieldedEncryptedNotes", + unchecked_return_type = "ShieldedEncryptedNote[]" + )] + 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 = "Uint8Array[]")] + 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")] + pub async fn get_most_recent_shielded_anchor( + &self, + ) -> Result, WasmSdkError> { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{MostRecentShieldedAnchor, NoParamQuery}; + + let result = MostRecentShieldedAnchor::fetch(self.as_ref(), NoParamQuery {}).await?; + Ok(result.map(|anchor| Uint8Array::from(anchor.0.as_slice()))) + } + + /// Checks the spent/unspent status of one or more nullifiers. + #[wasm_bindgen( + js_name = "getShieldedNullifiers", + unchecked_return_type = "ShieldedNullifierStatus[]" + )] + pub async fn get_shielded_nullifiers( + &self, + #[wasm_bindgen(unchecked_param_type = "Uint8Array[]")] nullifiers: js_sys::Array, + ) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{ShieldedNullifierStatuses, ShieldedNullifiersQuery}; + + let nullifier_arrays: Vec<[u8; 32]> = nullifiers + .iter() + .enumerate() + .map(|(i, v)| { + wasm_dpp2::utils::try_to_fixed_bytes::<32>(v, &format!("nullifiers[{}]", i)) + .map_err(WasmSdkError::from) + }) + .collect::>()?; + + 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::NULL); + + 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::NULL); + + 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, + #[wasm_bindgen(unchecked_param_type = "Uint8Array[]")] nullifiers: js_sys::Array, + ) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{ShieldedNullifierStatuses, ShieldedNullifiersQuery}; + + let nullifier_arrays: Vec<[u8; 32]> = nullifiers + .iter() + .enumerate() + .map(|(i, v)| { + wasm_dpp2::utils::try_to_fixed_bytes::<32>(v, &format!("nullifiers[{}]", i)) + .map_err(WasmSdkError::from) + }) + .collect::>()?; + + 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, + )) + } +} diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs index fc2816e8f88..be9c2e9aa71 100644 --- a/packages/wasm-sdk/src/state_transitions/addresses.rs +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -738,8 +738,9 @@ impl WasmSdk { // Deserialize simple fields last (consumes options) let parsed = deserialize_address_funding_options(options.into())?; - // Convert outputs to map (address -> optional amount) - let outputs_map = outputs_to_optional_btree_map(parsed.outputs); + // Convert outputs to map (address -> optional amount). + // `outputs_to_optional_btree_map` is fallible (rejects duplicate addresses). + let outputs_map = outputs_to_optional_btree_map(parsed.outputs)?; // Convert fee strategy from input using wasm-dpp2 helper let fee_strategy = fee_strategy_from_steps_or_default(parsed.fee_strategy); diff --git a/packages/wasm-sdk/tests/functional/shielded.spec.ts b/packages/wasm-sdk/tests/functional/shielded.spec.ts new file mode 100644 index 00000000000..1fe4a1a86c0 --- /dev/null +++ b/packages/wasm-sdk/tests/functional/shielded.spec.ts @@ -0,0 +1,181 @@ +import { expect } from './helpers/chai.ts'; +import init, * as sdk from '../../dist/sdk.compressed.js'; + +// These tests run against a local dashmate node and verify only the +// SHAPE of each shielded query response, not its contents — the local +// pool may legitimately be empty (no shielded transactions yet) and +// the client must still return a well-formed value, not throw. + +describe('Shielded queries', function describeShielded() { + this.timeout(90000); + + let client: sdk.WasmSdk; + + before(async () => { + await init(); + const context = await sdk.WasmTrustedContext.prefetchLocal(); + const builder = sdk.WasmSdkBuilder.local().withTrustedContext(context); + client = await builder.build(); + }); + + after(() => { + if (client) { client.free(); } + }); + + // ── Pool state ──────────────────────────────────────────────────────── + + describe('getShieldedPoolState()', () => { + it('should return bigint or undefined', async () => { + const result = await client.getShieldedPoolState(); + // Pool may be empty (undefined) or non-empty (bigint). + if (result !== undefined) { + expect(result).to.be.a('bigint'); + } + }); + }); + + describe('getShieldedPoolStateWithProofInfo()', () => { + it('should return data + metadata + proof', async () => { + const response = await client.getShieldedPoolStateWithProofInfo(); + + expect(response).to.be.ok(); + expect(response.metadata).to.be.ok(); + expect(response.proof).to.be.ok(); + // data is bigint | null (null preserves the field in JSON.stringify) + if (response.data !== null) { + expect(response.data).to.be.a('bigint'); + } + }); + }); + + // ── Encrypted notes ────────────────────────────────────────────────── + + describe('getShieldedEncryptedNotes()', () => { + it('should return an array of ShieldedEncryptedNote (possibly empty)', async () => { + const notes = await client.getShieldedEncryptedNotes(0n, 10); + + expect(notes).to.be.an('array'); + for (const note of notes) { + expect(note).to.be.instanceOf(sdk.ShieldedEncryptedNote); + expect(note.cmx).to.be.instanceOf(Uint8Array); + expect(note.nullifier).to.be.instanceOf(Uint8Array); + expect(note.encryptedNote).to.be.instanceOf(Uint8Array); + } + }); + }); + + describe('getShieldedEncryptedNotesWithProofInfo()', () => { + it('should return data array + metadata + proof', async () => { + const response = await client.getShieldedEncryptedNotesWithProofInfo(0n, 10); + + expect(response).to.be.ok(); + expect(response.metadata).to.be.ok(); + expect(response.proof).to.be.ok(); + expect(response.data).to.be.an('array'); + }); + }); + + // ── Anchors ────────────────────────────────────────────────────────── + + describe('getShieldedAnchors()', () => { + it('should return an array of Uint8Array (possibly empty)', async () => { + const anchors = await client.getShieldedAnchors(); + + expect(anchors).to.be.an('array'); + for (const anchor of anchors) { + expect(anchor).to.be.instanceOf(Uint8Array); + expect(anchor.length).to.equal(32); + } + }); + }); + + describe('getShieldedAnchorsWithProofInfo()', () => { + it('should return data array + metadata + proof', async () => { + const response = await client.getShieldedAnchorsWithProofInfo(); + + expect(response).to.be.ok(); + expect(response.metadata).to.be.ok(); + expect(response.proof).to.be.ok(); + expect(response.data).to.be.an('array'); + }); + }); + + describe('getMostRecentShieldedAnchor()', () => { + it('should return Uint8Array (32 bytes) or undefined', async () => { + const anchor = await client.getMostRecentShieldedAnchor(); + + if (anchor !== undefined) { + expect(anchor).to.be.instanceOf(Uint8Array); + expect(anchor.length).to.equal(32); + } + }); + }); + + describe('getMostRecentShieldedAnchorWithProofInfo()', () => { + it('should return data + metadata + proof (data may be null)', async () => { + const response = await client.getMostRecentShieldedAnchorWithProofInfo(); + + expect(response).to.be.ok(); + expect(response.metadata).to.be.ok(); + expect(response.proof).to.be.ok(); + if (response.data !== null) { + expect(response.data).to.be.instanceOf(Uint8Array); + expect(response.data.length).to.equal(32); + } + }); + }); + + // ── Nullifiers ─────────────────────────────────────────────────────── + + describe('getShieldedNullifiers()', () => { + it('should reject empty input (server-side InvalidArgument)', async () => { + let err: Error | undefined; + try { + await client.getShieldedNullifiers([]); + } catch (e) { + err = e as Error; + } + expect(err, 'expected getShieldedNullifiers([]) to throw').to.exist(); + expect(err!.message).to.match(/nullifiers list must not be empty|invalid argument/i); + }); + + it('should return an entry per queried nullifier', async () => { + // Two arbitrary 32-byte nullifiers; the local pool almost certainly + // hasn't seen them, but the query should succeed and report + // isSpent: false for each. + const nullifiers = [new Uint8Array(32).fill(0xaa), new Uint8Array(32).fill(0xbb)]; + const statuses = await client.getShieldedNullifiers(nullifiers); + + expect(statuses).to.be.an('array'); + for (const status of statuses) { + expect(status).to.be.instanceOf(sdk.ShieldedNullifierStatus); + expect(status.nullifier).to.be.instanceOf(Uint8Array); + expect(status.nullifier.length).to.equal(32); + expect(status.isSpent).to.be.a('boolean'); + } + }); + + it('should reject Uint8Array of wrong length', async () => { + let threw = false; + try { + await client.getShieldedNullifiers([new Uint8Array(20)]); + } catch (_e) { + threw = true; + } + expect(threw).to.equal(true); + }); + }); + + describe('getShieldedNullifiersWithProofInfo()', () => { + it('should return data array + metadata + proof', async () => { + const response = await client.getShieldedNullifiersWithProofInfo([ + new Uint8Array(32).fill(0xcc), + ]); + + expect(response).to.be.ok(); + expect(response.metadata).to.be.ok(); + expect(response.proof).to.be.ok(); + expect(response.data).to.be.an('array'); + }); + }); +}); diff --git a/packages/wasm-sdk/tests/unit/shielded.spec.ts b/packages/wasm-sdk/tests/unit/shielded.spec.ts new file mode 100644 index 00000000000..f7e21792236 --- /dev/null +++ b/packages/wasm-sdk/tests/unit/shielded.spec.ts @@ -0,0 +1,133 @@ +import { expect } from './helpers/chai.ts'; +import init, * as sdk from '../../dist/sdk.compressed.js'; + +// `cmx`, `nullifier`, `encryptedNote` use `bytes_b64` which emits raw bytes +// in non-human-readable mode (Object) and base64 strings in human-readable +// mode (JSON). These tests pin both representations so the round-trip stays +// correct if the helper is ever refactored. + +describe('ShieldedEncryptedNote', () => { + before(async () => { + await init(); + }); + + const cmxBytes = new Uint8Array(32).fill(0xa1); + const nullifierBytes = new Uint8Array(32).fill(0xb2); + const encryptedNoteBytes = new Uint8Array(216).fill(0xc3); + + // Same bytes encoded as base64 for the JSON form. + const objectFixture = { + cmx: cmxBytes, + nullifier: nullifierBytes, + encryptedNote: encryptedNoteBytes, + }; + + describe('fromObject() and getters', () => { + it('should expose the three byte fields via getters', () => { + const note = sdk.ShieldedEncryptedNote.fromObject(objectFixture); + + expect(note.cmx).to.be.instanceOf(Uint8Array); + expect(note.cmx).to.deep.equal(cmxBytes); + expect(note.nullifier).to.deep.equal(nullifierBytes); + expect(note.encryptedNote).to.deep.equal(encryptedNoteBytes); + }); + }); + + describe('toObject()', () => { + it('should round-trip the bytes through fromObject/toObject', () => { + const note = sdk.ShieldedEncryptedNote.fromObject(objectFixture); + const obj = note.toObject(); + + expect(obj.cmx).to.deep.equal(cmxBytes); + expect(obj.nullifier).to.deep.equal(nullifierBytes); + expect(obj.encryptedNote).to.deep.equal(encryptedNoteBytes); + }); + }); + + describe('toJSON() / fromJSON()', () => { + it('should encode the byte fields as base64 strings', () => { + const note = sdk.ShieldedEncryptedNote.fromObject(objectFixture); + const json = note.toJSON(); + + expect(json.cmx).to.be.a('string'); + expect(json.nullifier).to.be.a('string'); + expect(json.encryptedNote).to.be.a('string'); + }); + + it('should round-trip via JSON', () => { + const note = sdk.ShieldedEncryptedNote.fromObject(objectFixture); + const json = note.toJSON(); + const restored = sdk.ShieldedEncryptedNote.fromJSON(json); + + expect(restored.cmx).to.deep.equal(cmxBytes); + expect(restored.nullifier).to.deep.equal(nullifierBytes); + expect(restored.encryptedNote).to.deep.equal(encryptedNoteBytes); + }); + }); +}); + +describe('ShieldedNullifierStatus', () => { + before(async () => { + await init(); + }); + + const nullifierBytes = new Uint8Array(32).fill(0xde); + + describe('fromObject() and getters', () => { + it('should expose nullifier bytes and isSpent flag', () => { + const status = sdk.ShieldedNullifierStatus.fromObject({ + nullifier: nullifierBytes, + isSpent: true, + }); + + expect(status.nullifier).to.be.instanceOf(Uint8Array); + expect(status.nullifier).to.deep.equal(nullifierBytes); + expect(status.isSpent).to.equal(true); + }); + + it('should accept isSpent: false', () => { + const status = sdk.ShieldedNullifierStatus.fromObject({ + nullifier: nullifierBytes, + isSpent: false, + }); + expect(status.isSpent).to.equal(false); + }); + }); + + describe('toObject()', () => { + it('should round-trip the nullifier and flag', () => { + const status = sdk.ShieldedNullifierStatus.fromObject({ + nullifier: nullifierBytes, + isSpent: true, + }); + const obj = status.toObject(); + + expect(obj.nullifier).to.deep.equal(nullifierBytes); + expect(obj.isSpent).to.equal(true); + }); + }); + + describe('toJSON() / fromJSON()', () => { + it('should encode nullifier as base64 string', () => { + const status = sdk.ShieldedNullifierStatus.fromObject({ + nullifier: nullifierBytes, + isSpent: false, + }); + const json = status.toJSON(); + + expect(json.nullifier).to.be.a('string'); + expect(json.isSpent).to.equal(false); + }); + + it('should round-trip via JSON', () => { + const status = sdk.ShieldedNullifierStatus.fromObject({ + nullifier: nullifierBytes, + isSpent: true, + }); + const restored = sdk.ShieldedNullifierStatus.fromJSON(status.toJSON()); + + expect(restored.nullifier).to.deep.equal(nullifierBytes); + expect(restored.isSpent).to.equal(true); + }); + }); +});