Skip to content

feat(wasm-sdk): add prepare_* APIs for idempotent document state transitions#3091

Draft
thepastaclaw wants to merge 25 commits into
dashpay:v3.1-devfrom
thepastaclaw:feat/sdk-prepare-document-apis
Draft

feat(wasm-sdk): add prepare_* APIs for idempotent document state transitions#3091
thepastaclaw wants to merge 25 commits into
dashpay:v3.1-devfrom
thepastaclaw:feat/sdk-prepare-document-apis

Conversation

@thepastaclaw
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw commented Feb 17, 2026

Issue

Closes #3090

Problem

The high-level document APIs (documentCreate, documentReplace, documentDelete) in the WASM SDK atomically bundle nonce management, ST construction, signing, broadcasting, and waiting. On timeout, callers cannot rebroadcast the same signed ST — retrying creates a duplicate with a new nonce.

Solution

Implements Option A (Two-Phase API) from the issue: add prepare_* variants for each document operation that return a signed StateTransition without broadcasting:

  • prepareDocumentCreate() — build, sign, return ST
  • prepareDocumentReplace() — build, sign, return ST
  • prepareDocumentDelete() — build, sign, return ST

These pair with the already-existing broadcastStateTransition() and waitForResponse() methods in broadcast.rs.

Usage Pattern

// 1. Prepare — get a signed StateTransition
const st = await sdk.prepareDocumentCreate({
  document, identityKey, signer
});

// 2. Cache for retry safety
const stBytes = st.toBytes();

// 3. Broadcast + wait
try {
  await sdk.broadcastStateTransition(st);
  const result = await sdk.waitForResponse(st);
} catch (e) {
  if (isTimeout(e)) {
    // 4. On timeout — deserialize and rebroadcast the IDENTICAL ST
    const cachedSt = StateTransition.fromBytes(stBytes);
    await sdk.broadcastStateTransition(cachedSt);
    const result = await sdk.waitForResponse(cachedSt);
  }
}

This gives applications full control over retry and caching strategy while leveraging Platform's built-in duplicate ST rejection.

Changes

  • packages/wasm-sdk/src/state_transitions/document.rs:
    • Added prepareDocumentCreate() — builds and signs a create ST without broadcasting
    • Added prepareDocumentReplace() — builds and signs a replace ST without broadcasting
    • Added prepareDocumentDelete() — builds and signs a delete ST without broadcasting
    • Added strict rs-sdk-backed create/replace prepare helpers that build, sign, validate, and roll back identity-contract nonce allocation on pre-broadcast failures
    • Added TypeScript interface definitions for all three prepare option types
    • Added module-level documentation explaining the two-phase pattern

Compatibility note: one-shot revision guards

The existing one-shot documentCreate() and documentReplace() APIs remain available, but this PR now rejects revision/API mismatches instead of silently routing them through the opposite transition type. In particular:

  • documentCreate() accepts an unset revision or INITIAL_REVISION; documents with any other explicit revision now fail with InvalidArgument instead of being treated as replace transitions.
  • documentReplace() requires a revision greater than INITIAL_REVISION; missing, zero, or initial revisions now fail with InvalidArgument instead of being treated as create transitions.

Consumers that reused stale documents or relied on the previous silent create/replace routing should audit those call sites before upgrading.

Rust SDK consumers should also note the related PutDocument::put_to_platform tightening: replace calls with revision = Some(0) are now rejected instead of being routed through the create branch, and create calls that pass document_state_transition_entropy = Some(entropy) now validate that document.id matches Document::generate_document_id_v0(...) before nonce allocation. The in-tree rs-platform-wallet caller uses revision: None for creates and Some(current_revision + 1) for updates, so it is unaffected.

Compatibility note: structure validation error ordering

ensure_valid_state_transition_structure now treats DPP's structure-validation sentinel (UnsupportedFeatureError entries whose feature name starts with "structure validation") as a no-op only when every error is that sentinel. Mixed validation results are not filtered: every original diagnostic is preserved, but non-UnsupportedFeatureError failures are reordered ahead of unsupported-feature entries so the typed primary error returned to callers is the actionable validation failure. Non-sentinel UnsupportedFeatureError entries (for example, unsupported sub-features inside an otherwise validated transition) still surface as real errors.

Compatibility note: native document builders

Native rs-sdk document create/replace builders now honor PutSettings.user_fee_increase and PutSettings.state_transition_creation_options passed through with_settings(...) when dedicated builder setters were not used, matching the delete builder and high-level SDK paths. Delete builder sign(...) now rolls back allocated identity-contract nonces on pre-broadcast failures, and sign_with_nonce(...) runs local structure validation before returning.

Testing

The existing document operation tests validate the build/sign/broadcast pipeline. The prepare variants reuse the same construction logic, stopping before broadcast. The broadcastStateTransition and waitForResponse methods are already tested in broadcast.rs. Manual testing with the yappr application (which prompted this issue) confirms the two-phase pattern works correctly.

Validation

Build verification

# WASM SDK builds successfully with new prepare_* APIs
cargo check -p wasm-sdk

Existing test coverage

The prepare_* methods reuse the same internal construction paths as the existing all-in-one document operations:

  • prepareDocumentCreate / prepareDocumentReplace delegate to strict rs-sdk helpers that validate create-vs-replace intent, validate create entropy/document-id consistency, allocate the identity-contract nonce, build, sign, and roll back the nonce on pre-broadcast failures
  • prepareDocumentDelete uses the shared rs-sdk delete helper, the same build/sign/validate path used by the existing documentDelete flow
  • broadcastStateTransition() and waitForResponse() (the "execute" half) are already tested in broadcast.rs

Existing CI test suites (cargo test -p wasm-sdk, platform integration tests) validate these shared code paths.

Manual / integration testing

Input validation

  • prepareDocumentReplace rejects documents with INITIAL_REVISION (guards against accidental create-as-replace)
  • prepareDocumentCreate validates entropy is present and exactly 32 bytes
  • prepareDocumentDelete accepts both full Document instances and minimal identifier objects

Summary by CodeRabbit

  • New Features
    • Two-phase document state transitions: prepare and execute flows.
    • New prepare actions for create, replace, and delete documents.
    • Prepared transitions are built and signed locally (not broadcast) to allow idempotent retries and separate signing.
    • Delete prepare accepts richer or partial document input.
    • Existing one-shot create/replace/delete flows remain available.

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(sdk): high-level document APIs lack idempotent retry — timeout causes duplicate state transitions

5 participants