Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
c5325c9
feat(wasm-dpp2,js-sdk): add shielded pool WASM bindings and JS SDK me…
QuantumExplorer Mar 12, 2026
65c02a6
revert: remove js-dash-sdk and wasm-dpp changes from shielded PR
QuantumExplorer Mar 12, 2026
8b71f68
feat(wasm-sdk): add shielded pool query methods
QuantumExplorer Mar 12, 2026
3256346
feat(wasm-dpp2): implement shielded proof result wrappers
QuantumExplorer Mar 12, 2026
d08e087
fix(wasm-sdk): correct computePlatformSighash doc for extraData format
QuantumExplorer Mar 12, 2026
98a20b6
fix(wasm-sdk): remove needless borrow in computePlatformSighash
QuantumExplorer Mar 12, 2026
5d4ee20
fix(wasm): address PR review feedback for shielded bindings
QuantumExplorer Mar 13, 2026
35b7ba4
fix(wasm-sdk): reject nullifiers that are not exactly 32 bytes
QuantumExplorer Mar 13, 2026
a87c6ad
Merge branch 'v3.1-dev' into feat/zk-wasm-js-bindings
QuantumExplorer Mar 13, 2026
0b48cd3
Merge branch 'v3.1-dev' into feat/zk-wasm-js-bindings
QuantumExplorer Mar 16, 2026
74e9290
feat(wasm): address all review feedback on shielded WASM bindings
QuantumExplorer Mar 16, 2026
7f216ec
feat(wasm): improve TypeScript interface definitions per shumkov review
QuantumExplorer Mar 16, 2026
c6a73b7
fix(wasm): use base64 string for all byte fields in OrchardActionJSON
QuantumExplorer Mar 16, 2026
4e29ccd
fix(wasm): use number | string for BigInt fields in JSON interfaces
QuantumExplorer Mar 16, 2026
fd850e1
fix(wasm): use typed parameters in shielded transition constructors
QuantumExplorer Mar 16, 2026
2db3796
fix(wasm): correct TS types to match actual serde output
QuantumExplorer Mar 16, 2026
c55a943
Merge branch 'v3.1-dev' into feat/zk-wasm-js-bindings
QuantumExplorer Mar 17, 2026
c6a2b3f
fix(wasm): change $version to $formatVersion, add Options constructor
QuantumExplorer Mar 17, 2026
df3de08
refactor(wasm-dpp2): unify rs-dpp/wasm-dpp2 typing and conversion con…
shumkov Apr 28, 2026
05e96f4
fix(wasm-dpp2): replace bare object types with typed JSON externs
shumkov Apr 28, 2026
26d7d0c
refactor(wasm-dpp2): convert shielded getters to property style
shumkov Apr 28, 2026
a84bfd6
Merge remote-tracking branch 'origin/v3.1-dev' into feat/zk-wasm-js-b…
shumkov Apr 28, 2026
42d02a1
style(wasm-dpp2): apply rustfmt
shumkov Apr 28, 2026
f200b65
fix(ci): resolve eslint and clippy lints surfaced by full CI run
shumkov Apr 28, 2026
321b18c
fix(platform-wallet): update spv_sync test to flat CoreChangeSet shape
shumkov Apr 28, 2026
c48b224
test(platform-value): lock in BinaryData byte-input deserialization
shumkov Apr 28, 2026
7ecdf5e
fix(wasm-dpp2): type outputAddress as Uint8Array in Object form + str…
shumkov Apr 29, 2026
f6ce62d
fix(dpp,wasm-dpp2): emit pooling as a string in JSON, fix broken TS t…
shumkov Apr 29, 2026
e87d363
fix(wasm-dpp2): enforce 216-byte encryptedNote at the boundary (mirro…
shumkov Apr 29, 2026
e3b301c
revert(wasm-dpp2): remove encryptedNote 216-byte check, codify thin-w…
shumkov Apr 29, 2026
19d7f54
refactor(wasm-dpp2): remove wasm-side size caps, defer to DPP validation
shumkov Apr 29, 2026
1dc3fbf
fix(wasm-dpp2): preserve Map entries in toJSON for verified-result wr…
shumkov Apr 29, 2026
cad8e46
fix(dpp): align address_funds::serde_helpers feature gate with module…
shumkov Apr 29, 2026
b9abce9
feat(js-evo-sdk): add ShieldedFacade for shielded pool queries
shumkov Apr 29, 2026
e82fc64
test(wasm-sdk): add unit tests for shielded query result types
shumkov Apr 29, 2026
1f7eef5
style(dpp): fmt pooling_serde Visitor and discriminant error
shumkov Apr 29, 2026
4a4b8f8
fix(dpp): satisfy clippy on serde_helpers gating and pooling_serde im…
shumkov Apr 29, 2026
e6b3655
test(wasm-sdk): add functional tests for shielded query methods
shumkov Apr 29, 2026
b78a6e9
refactor(wasm-sdk): drop redundant IShielded* TS interfaces in querie…
shumkov Apr 29, 2026
51da814
refactor(wasm-sdk): apply json_safe_fields convention to shielded wra…
shumkov Apr 29, 2026
ec2bcf2
chore(dpp): make serde_bytes and serde_bytes_var modules public
shumkov Apr 29, 2026
04cfcc4
refactor(wasm-sdk,js-evo-sdk): null/undefined convention for shielded…
shumkov Apr 29, 2026
624d3f7
fix(dpp): make serde_bytes / serde_bytes_var deserialize accept byte …
shumkov Apr 29, 2026
9d8d45b
refactor(wasm-sdk): drop bytes_b64 in shielded.rs, codify round-trip …
shumkov Apr 29, 2026
9acf091
refactor(wasm-sdk): use wasm_dpp2::utils::try_to_fixed_bytes in parse…
shumkov Apr 29, 2026
130fbda
refactor(wasm-sdk,js-evo-sdk): tighten Array typing on shielded queries
shumkov Apr 29, 2026
f8fa60e
style(wasm-sdk): fmt single-line wasm_bindgen attribute on getShielde…
shumkov Apr 29, 2026
607726e
refactor(wasm-dpp2): split proof_result into per-domain submodules
shumkov Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
# ignore VSCode project specific files
.vscode

# ignore Serena MCP project-local cache/config
.serena

# Env file
.env

Expand Down
4 changes: 4 additions & 0 deletions packages/js-evo-sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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';
86 changes: 86 additions & 0 deletions packages/js-evo-sdk/src/shielded/facade.ts
Original file line number Diff line number Diff line change
@@ -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<bigint | undefined> {
const w = await this.sdk.getWasmSdkConnected();
return w.getShieldedPoolState();
}

async poolStateWithProof(): Promise<wasm.ProofMetadataResponseTyped<bigint | null>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getShieldedPoolStateWithProofInfo();
}

// ── Encrypted notes ──────────────────────────────────────────────────

async encryptedNotes(startIndex: bigint, count: number): Promise<wasm.ShieldedEncryptedNote[]> {
const w = await this.sdk.getWasmSdkConnected();
return w.getShieldedEncryptedNotes(startIndex, count);
}

async encryptedNotesWithProof(
startIndex: bigint,
count: number,
): Promise<wasm.ProofMetadataResponseTyped<wasm.ShieldedEncryptedNote[]>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getShieldedEncryptedNotesWithProofInfo(startIndex, count);
}

// ── Anchors ──────────────────────────────────────────────────────────

async anchors(): Promise<Uint8Array[]> {
const w = await this.sdk.getWasmSdkConnected();
return w.getShieldedAnchors();
}

async anchorsWithProof(): Promise<wasm.ProofMetadataResponseTyped<Uint8Array[]>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getShieldedAnchorsWithProofInfo();
}

async mostRecentAnchor(): Promise<Uint8Array | undefined> {
const w = await this.sdk.getWasmSdkConnected();
return w.getMostRecentShieldedAnchor();
}

async mostRecentAnchorWithProof(): Promise<wasm.ProofMetadataResponseTyped<Uint8Array | null>> {
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<wasm.ShieldedNullifierStatus[]> {
const w = await this.sdk.getWasmSdkConnected();
return w.getShieldedNullifiers(nullifiers);
}

async nullifiersWithProof(
nullifiers: Uint8Array[],
): Promise<wasm.ProofMetadataResponseTyped<wasm.ShieldedNullifierStatus[]>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getShieldedNullifiersWithProofInfo(nullifiers);
}
}
121 changes: 121 additions & 0 deletions packages/js-evo-sdk/tests/unit/facades/shielded.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
74 changes: 64 additions & 10 deletions packages/rs-dpp-json-convertible-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>`, `Option<i64>`, 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<u64>` 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<i64>` 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<u8>` | `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<u8>, // → auto-annotated with crate::serialization::serde_bytes_var
/// pub name: String, // → untouched
/// }
/// ```
Expand Down Expand Up @@ -290,12 +315,18 @@ fn annotate_fields(fields: &mut syn::FieldsNamed, base_path: &str) -> Vec<Type>

/// Determine if a type needs a serde `with` annotation and return the module name suffix.
///
/// Matches literal `u64`, `i64`, `Option<u64>`, `Option<i64>`, and known type aliases
/// that resolve to u64/i64 (e.g. `Credits`, `TokenAmount`, `TimestampMillis`).
/// Matches:
/// - `u64`, `i64`, `Option<u64>`, `Option<i64>`, 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<u8>` → `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`
Expand All @@ -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<u8> → 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<u64/i64/alias> — handles both `Option<T>` and `std::option::Option<T>`
if ident == "Option" {
if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
Expand Down Expand Up @@ -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<u8>`
/// 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`.
///
Expand Down
Loading
Loading