Skip to content

feat: native state-transition deserialization, plus document/put/ContractBounds fixes - #40

Merged
HashEngineering merged 11 commits into
mainfrom
fix/contract-bounds
Aug 14, 2026
Merged

feat: native state-transition deserialization, plus document/put/ContractBounds fixes#40
HashEngineering merged 11 commits into
mainfrom
fix/contract-bounds

Conversation

@HashEngineering

@HashEngineering HashEngineering commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Nine commits from implementing the first wallet-side DashConnect / key-exchange login
(dash-wallet MO-945), which needed SDK capabilities that did not exist yet.

Native state-transition deserialization (the main addition)

The Kotlin StateTransitionFactory is CBOR-only, so it cannot decode transitions produced
by the Rust/wasm SDKs (bincode with platform versioning). A wallet receiving a serialized
IdentityUpdateTransition in a QR code had no way to inspect it before signing.

  • platform-mobile/src/state_transition.rs: deserialize_state_transition(bytes) using DPP's
    native deserialize_from_bytes, returning a StateTransitionInfo with the transition type,
    name, owner id, revision, identity nonce, and — for identity updates — the added public keys
    as existing IdentityPublicKey values (no new FFI type).
  • NativeStateTransition.kt: Kotlin wrapper, result mapped through base.Result via
    DEFINE_RESULT/DEFINE_LIST_RESULT so memory is managed by the typemaps like every other
    SDK call, rather than a raw ferment wrapper with a manual destroy.
  • ignore.i / clone.h regenerated (via ignore.py), and dash-sdk-android's fermented.rs
    synced with the new exports — without that the Android cmake link fails on undefined symbols.

Fixes found while using the SDK from the wallet

  • Document.type was null on queried documents. Only the create path set it, and a null
    type segfaults platformMobilePutReplaceDocumentSdk through JNI. The type is now carried on
    documents built from native results.
  • put.rs panicked on bad input. .expect() calls replaced with Err returns, so invalid
    arguments surface as errors instead of aborting the process.
  • ContractBounds was dropped in IdentityPublicKey CBOR serialization, so contract-bound
    keys did not round-trip. Covered by new IdentityPublicKeyTest and IdentitySelectKeyTest.
  • 32-bit native libs excluded, as they crash.

Verification

Published as 4.0.1-SNAPSHOT and consumed by the dash-wallet DashConnect branch, where the
full login flow completes end to end on testnet: a scanned IdentityUpdateTransition is
deserialized, verified to target our identity and to add exactly our derived login keys, and
the two keys are then registered on the identity.

Note for reviewers: yappr serializes that transition without the outer StateTransition
enum variant tag (wasm-dpp2's IdentityUpdateTransition::to_bytes() serializes the bare inner
transition), so callers may need to handle both framings. Exposing
IdentityUpdateTransition::deserialize_from_bytes directly would be a natural follow-up.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added state-transition deserialization with transition details and newly added identity keys.
    • Added support for contract-bound identity keys, contract bounds, and contract-specific key selection.
    • Document data now preserves its document type more reliably.
    • Added Java support for state-transition data.
  • Bug Fixes

    • Improved contact-request encryption key selection.
    • Replaced certain document-operation crashes with descriptive errors.
  • Compatibility

    • Android builds now support 64-bit ARM and x86_64 devices only.

HashEngineering and others added 9 commits July 11, 2026 14:47
IdentityPublicKey.toObject() stored the raw ContractBounds object in the
map, so Cbor.encode() threw "No converter for SingleContractDocumentType"
when serializing any identity whose key carried SingleContractDocumentType
bounds. This crashed sending/accepting a DashPay contact request via
PlatformStateRepository.storeIdentity -> identity.toBuffer().toHex().

- toObject() now emits contractBounds.toObject() (a nested Map); the Map
  constructor reconstructs typed bounds via ContractBounds.from(Map).
- ContractBounds gains from(Map) and toNative(), and toNative() now carries
  the bounds through the Rust SDK instead of dropping them.
- storeIdentity's toBuffer() log is wrapped in try/catch so serialization
  can never break fetchIdentity again.
- ContactRequests prefers a recipient encryption key whose contract bounds
  are scoped to the DashPay contract (new Identity.getFirstPublicKey overload).
- Regression + selection tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d put input

Two fixes surfaced by the DashPay wallet's DashConnect feature (first consumer
of the mobile document put/replace path):

- Document(RustDocument, dataContractId) left `type` null because the native
  document carries no type name. Add a `type` parameter and pass it from the
  DapiClient query (which already knows the type). A null type breaks any
  subsequent replace/put, whose native calls require a non-null type string.

- put_document_sdk / replace_document_sdk used .expect() on
  document_type_for_name and CallbackSigner::new, aborting the whole process
  on a missing type or bad signer. Return an Err string instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a ferment-exported deserialize_state_transition(bytes) -> StateTransitionInfo
to platform-mobile. Modern state transitions (produced by the Rust SDK / wasm-sdk)
are bincode-serialized, which the legacy Kotlin StateTransitionFactory (CBOR-only,
and lacking IdentityUpdate support) cannot decode. This gives callers a native
path to inspect a serialized transition's type, name, and owner id.

StateTransitionInfo is intentionally minimal; extend with added-public-keys /
revision / nonce as callers need them.

fermented.rs regenerated (ferment full mode) with the new binding; the two
broken platform_mobile_provider_Cache FFI conversion impls that ferment v0.2.3
mis-generates were removed, as before. Builds clean in cbindgen_only mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NativeStateTransition.deserialize(bytes) calls the new
platformMobileStateTransitionDeserializeStateTransition FFI binding and maps
the result to a StateTransitionInfo (type, name, ownerId), freeing the native
struct afterward. Gives Kotlin callers a working path to decode modern
(bincode) transitions, which the CBOR-only StateTransitionFactory cannot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

Extend the native deserializer to expose all the information carried by a
dash-st (IdentityUpdate) transition:

- StateTransitionInfo gains revision and identity_nonce
- identity_update_public_keys_to_add(bytes) returns the added keys as the
  standard IdentityPublicKey type (converted from IdentityPublicKeyInCreation),
  reusing the existing IdentityPublicKey ferment/SWIG/clone plumbing rather
  than a bespoke struct

SWIG: DEFINE_LIST_RESULT maps Result<Vec<IdentityPublicKey>> to
Result<List<IdentityPublicKey>> (state_transition.i, wired into root.i).

Kotlin: NativeStateTransition.deserialize now returns revision, identityNonce
and addPublicKeys (List<IdentityPublicKey>). fermented.rs regenerated; the
mis-generated Cache FFI impls removed as before. Builds end-to-end
(:dpp:compileKotlin incl. native link).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ignore.i/clone.h

Follow the canonical SWIG pattern: deserialize_state_transition now maps through
DEFINE_RESULT to base.Result<StateTransitionInfo> (rather than the raw ferment
Result wrapper with a manual destroy), so memory is managed by the typemap like
every other SDK call. This requires StateTransitionInfo: Clone plus a
StateTransitionInfo_clone export in clone.rs.

Regenerate ignore.i and clone.h via ignore.py (which must be run whenever the
header changes) so the new types' _ctor/_destroy ignores and the
StateTransitionInfo clone overload are present. The Kotlin wrapper drops the
manual destroy and just calls Result.unwrap().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…orts

dash-sdk-android/src/main/rust is a parallel dash_sdk_bindings crate over the
same platform-mobile; its committed fermented.rs was stale, so the Android
arm64 libdash_sdk_bindings.a lacked the new state-transition symbols and the
cmake link failed (undefined platform_mobile_state_transition_* /
StateTransitionInfo_clone). Regenerate it to match dash-sdk-bindings (identical
ferment config + shared platform-mobile). Host cargo check passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fa5d7a2f-691d-4d89-b355-692c9e0bd084

📥 Commits

Reviewing files that changed from the base of the PR and between c990d21 and d614e48.

📒 Files selected for processing (8)
  • dpp/src/main/java/org/dashj/platform/dapiclient/DapiClient.kt
  • dpp/src/main/java/org/dashj/platform/dashpay/ContactRequests.kt
  • dpp/src/main/java/org/dashj/platform/dashpay/Profiles.kt
  • dpp/src/main/java/org/dashj/platform/dpp/document/Document.kt
  • dpp/src/main/java/org/dashj/platform/dpp/identity/Identity.kt
  • dpp/src/main/java/org/dashj/platform/wallet/IdentityVerify.kt
  • dpp/src/main/java/org/dashj/platform/wallet/TxMetadata.kt
  • dpp/src/test/kotlin/org/dashj/platform/dpp/identity/IdentitySelectKeyTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • dpp/src/main/java/org/dashj/platform/dpp/identity/Identity.kt

📝 Walkthrough

Walkthrough

The PR adds native state-transition deserialization and Java FFI bindings. It preserves contract-bound identity keys, adds contract-aware key selection, retains document types, improves error handling, updates metadata tests, and limits Android builds to 64-bit ABIs.

Changes

SDK integration

Layer / File(s) Summary
Native state-transition deserialization
platform-mobile/src/state_transition.rs, platform-mobile/src/lib.rs, platform-mobile/src/clone.rs
Rust exports state-transition metadata deserialization, identity-update key extraction, module access, and cloning.
Java state-transition FFI bindings
dash-sdk-java/src/main/cpp/clone.h, dash-sdk-java/src/main/swig/state_transition.i, dash-sdk-java/src/main/swig/root.i, dash-sdk-java/src/main/swig/ignore.i, dpp/src/main/java/org/dashj/platform/dpp/statetransition/NativeStateTransition.kt
SWIG and JNI bindings expose native state-transition results and map errors, clones, and public-key lists to Java.
Contract-bound identity key handling
dpp/src/main/java/org/dashj/platform/dpp/identity/*, dpp/src/test/kotlin/org/dashj/platform/dpp/identity/*
Contract bounds support map and native conversions. Identity keys preserve bounds, and selection supports contract-aware filtering with tests.
Document type and DashPay integration
dpp/src/main/java/org/dashj/platform/dpp/document/Document.kt, dpp/src/main/java/org/dashj/platform/dapiclient/DapiClient.kt, dpp/src/main/java/org/dashj/platform/dashpay/*, dpp/src/main/java/org/dashj/platform/wallet/*
Document construction retains document types. Contact requests prefer contract-scoped encryption keys before fallback keys.
Error handling and build updates
platform-mobile/src/put.rs, dpp/src/main/java/org/dashj/platform/sdk/platform/PlatformStateRepository.kt, dpp/src/test/kotlin/org/dashj/platform/contracts/wallet/TxMetaDataTests.kt, dash-sdk-android/build.gradle
Document operations return errors instead of panicking, identity logging catches serialization failures, metadata tests include timestamps, and Android builds use 64-bit ABIs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: ⚪ Minimal · up to d614e

The current changes are merge-ready after normal checks and review; no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant JavaSDK
  participant SWIG
  participant PlatformMobile
  participant BincodeStateTransition
  JavaSDK->>SWIG: NativeStateTransition.deserialize(bytes)
  SWIG->>PlatformMobile: deserialize_state_transition(bytes)
  PlatformMobile->>BincodeStateTransition: Deserialize bincode bytes
  BincodeStateTransition-->>PlatformMobile: Transition data or error
  PlatformMobile-->>SWIG: StateTransitionInfo or String error
  SWIG-->>JavaSDK: Mapped Java result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main state-transition deserialization feature and the related document, put, and ContractBounds fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/contract-bounds

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dpp/src/main/java/org/dashj/platform/dashpay/ContactRequests.kt`:
- Around line 41-50: Update the fallback after the DashPay-scoped lookup in
getFirstPublicKey so it selects only enabled MEDIUM ECDSA_SECP256K1 encryption
keys with contractBounds == null, then retain the existing high-security
authentication-key fallback. Add a test covering an other-contract-bound key
preceding an unbound encryption key and verify the unbound key is selected.

In `@dpp/src/main/java/org/dashj/platform/dpp/document/Document.kt`:
- Around line 65-70: Require the type parameter in the RustDocument constructor
by removing its default value, then update DapiClient.deserializeDocument and
ContactRequests.create to pass the known document type when constructing native
Documents. Ensure every native-document construction site supplies a non-null
type for subsequent put or replace operations.

In `@platform-mobile/src/state_transition.rs`:
- Around line 36-38: Update deserialize_state_transition to use a shared decoder
that first supports tagged StateTransition payloads and falls back to bare
IdentityUpdateTransition payloads, preserving the existing StateTransitionInfo
result. Make identity_update_public_keys_to_add reuse this decoder, and add
regression tests covering both tagged and bare identity-update payloads.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fca11536-e251-48b0-80bb-8a345e1c474d

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb5c72 and c990d21.

📒 Files selected for processing (22)
  • dash-sdk-android/build.gradle
  • dash-sdk-android/src/main/rust/src/fermented.rs
  • dash-sdk-bindings/src/fermented.rs
  • dash-sdk-java/src/main/cpp/clone.h
  • dash-sdk-java/src/main/swig/ignore.i
  • dash-sdk-java/src/main/swig/root.i
  • dash-sdk-java/src/main/swig/state_transition.i
  • dpp/src/main/java/org/dashj/platform/dapiclient/DapiClient.kt
  • dpp/src/main/java/org/dashj/platform/dashpay/ContactRequests.kt
  • dpp/src/main/java/org/dashj/platform/dpp/document/Document.kt
  • dpp/src/main/java/org/dashj/platform/dpp/identity/ContractBounds.kt
  • dpp/src/main/java/org/dashj/platform/dpp/identity/Identity.kt
  • dpp/src/main/java/org/dashj/platform/dpp/identity/IdentityPublicKey.kt
  • dpp/src/main/java/org/dashj/platform/dpp/statetransition/NativeStateTransition.kt
  • dpp/src/main/java/org/dashj/platform/sdk/platform/PlatformStateRepository.kt
  • dpp/src/test/kotlin/org/dashj/platform/contracts/wallet/TxMetaDataTests.kt
  • dpp/src/test/kotlin/org/dashj/platform/dpp/identity/IdentityPublicKeyTest.kt
  • dpp/src/test/kotlin/org/dashj/platform/dpp/identity/IdentitySelectKeyTest.kt
  • platform-mobile/src/clone.rs
  • platform-mobile/src/lib.rs
  • platform-mobile/src/put.rs
  • platform-mobile/src/state_transition.rs

Comment thread dpp/src/main/java/org/dashj/platform/dashpay/ContactRequests.kt
Comment thread dpp/src/main/java/org/dashj/platform/dpp/document/Document.kt Outdated
Comment thread platform-mobile/src/state_transition.rs

@abaranouski abaranouski left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

approved, though the build failed on platform monorepo's Rust dependency

…back

getFirstPublicKey's bounds-agnostic fallback ignored contractBounds entirely,
so it could hand back a key scoped to an unrelated contract instead of one
with no contract bounds. Extend the contract-aware overload to accept a
nullable contractId (null = unbound only) and use it in the fallback.
The RustDocument-based Document constructor defaulted type to null,
so several put/replace call sites silently dropped the document type
after publishing, breaking any subsequent replace/put on that
document. Make type required and pass it through at every call site.
@HashEngineering
HashEngineering merged commit 49de14b into main Aug 14, 2026
2 checks passed
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.

2 participants