Skip to content

feat(sdk): propertyConstraints rules and pre-check in the Kotlin SDK and Android example app - #5066

Merged
QuantumExplorer merged 3 commits into
v4.2-devfrom
claude/property-constraints-kotlin
Sep 27, 2026
Merged

QuantumExplorer merged 3 commits into
v4.2-devfrom
claude/property-constraints-kotlin

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Basic explanation

What this does: The Kotlin SDK and the Android example app can now list a document type's propertyConstraints rules and check a new document against them before sending it. Two new JNI functions pass the contract bytes, the properties and the owner to the two C functions #5064 added to rs-sdk-ffi; Rust does all the reading and judging, and Kotlin only decodes the JSON that comes back.

Value: The Android app now matches the iOS app: it shows each rule on the document type screen, and it refuses to broadcast a document that breaks one, naming the rule and the reason. Without this, the user learns about the broken rule only after paying for a transition that consensus refuses with error 10422. All three SDKs (JS in #5051, Swift in #5064, Kotlin here) now report rules and violations in the same shape.

Risks: Low. Everything is additive: two new JNI exports, two new SDK methods, new data classes, one new screen section and one extra step before a create. Nothing changes in consensus, Drive, DPP or rs-sdk-ffi. If the check cannot run (no SDK, the contract was stored without its bytes, or the check fails), the app logs it and sends the document as before, so the new step never blocks a valid create. The Android native library must be rebuilt (./build_android.sh) for the app to use the new exports. With an older .so the app does not crash: the details screen says the library predates property constraints, and creates go ahead without the pre-check. Like the Swift side, the rules are read at the SDK's current protocol version, so before the app learns that the network is at 14 it sees no rules: the pre-check can then miss a broken rule (consensus still refuses it), but it never refuses a valid document.

Issue being fixed or feature implemented

The propertyConstraints series (#4962, then #5036 through #5048) gave consensus a rule language for document properties, but the Kotlin SDK only surfaced the 10422 error after the fact. This is the last of three SDK PRs:

  1. JS/WASM: #5051.
  2. Swift SDK and iOS example app, plus the two C functions: #5064.
  3. Kotlin SDK and Android example app: this PR.

The two C functions it calls came with #5064, now merged into v4.2-dev, which this PR targets.

What was done?

rs-unified-sdk-jni (src/queries.rs)

Two exports on QueriesNative, each a thin marshaler over one rs-sdk-ffi function, with the crate's usual guard, unwrap_string (throws DashSDKException and frees the error, or copies and frees the string) and jlong handle cast:

// -> dash_sdk_data_contract_get_property_constraints
Java_org_dashfoundation_dashsdk_ffi_QueriesNative_dataContractGetPropertyConstraints(
    env, class, sdk: jlong, serialized_contract: JByteArray, document_type: JString) -> jstring

// -> dash_sdk_data_contract_check_property_constraints
Java_org_dashfoundation_dashsdk_ffi_QueriesNative_dataContractCheckPropertyConstraints(
    env, class, sdk: jlong, serialized_contract: JByteArray, document_type: JString,
    properties_json: JString, owner_id: JByteArray) -> jstring

The contract bytes are copied into a buffer that outlives the call; the owner id must be exactly 32 bytes (the C function reads 32 bytes behind the pointer), otherwise InvalidParameter is thrown before the call. Error codes pass through unchanged, so Kotlin sees DashSdkError.NotFound for an unknown document type, SerializationError for bytes that are not a contract and InvalidParameter for empty bytes or properties that are not a JSON object.

Kotlin SDK

  • ffi/QueriesNative.kt: the two external fun declarations.

  • queries/PlatformQueries.kt: sdk.contracts.propertyConstraints(serializedContract, documentType): List<DocumentPropertyConstraint> and sdk.contracts.checkPropertyConstraints(serializedContract, documentType, propertiesJson, ownerId): PropertyConstraintViolation?. Both run under the SDK's query fence (queryGate) like every other Contracts call, since they read the SDK handle for its protocol version, and map native errors to DashSdkError.

  • queries/DocumentPropertyConstraints.kt: the data classes, decoded with kotlinx.serialization and matching the Swift types field for field:

    • DocumentPropertyConstraint(name, ruleJson, reads, readsOwner), with ruleJson kept as the declared rule (compact, sorted keys) and prettyRuleJson for display;
    • PropertyConstraintRead(path, kind), kind one of Value, Presence, Text, Identifier or Other(name);
    • PropertyConstraintViolation(rule, violation, message), violation one of NotMet, Overflow, DivisionByZero, NegativeExponent, NotAnInteger or Other(name), with a one-line description.

    A name a later version adds decodes to Other(name) instead of failing the list. Malformed JSON (a missing field, a number or a string where a boolean belongs) throws DashSdkError.SerializationError. No rule is evaluated in Kotlin.

Example:

val bytes = contractEntity.binarySerialization!!   // what fetchWithSerialization stored

sdk.contracts.propertyConstraints(bytes, "offer")
// [DocumentPropertyConstraint(name=perUnitFee,
//    ruleJson={"greaterThanOrEqual":[{"divide":["price","fee"]},1]},
//    reads=[PropertyConstraintRead(path=price, kind=Value), PropertyConstraintRead(path=fee, kind=Value)],
//    readsOwner=false), ...]

sdk.contracts.checkPropertyConstraints(bytes, "offer", """{"price":100,"fee":0}""", ownerId)
// PropertyConstraintViolation(rule=perUnitFee, violation=DivisionByZero, message=it divides by zero)

KotlinExampleApp (ui/contracts/)

  • PropertyConstraints.kt: the screen glue, kept out of the composables so it can be unit tested: whether a schema declares the keyword, what the details section shows (Hidden, Rules, NotEnforced, Unavailable(reason)), and the create pre-check outcome (Passed, Broken(violation), Skipped(reason)).

Document type details (DocumentTypeDetailsScreen.kt). Before: the screen listed settings, indices and properties; a type's rules were visible only in the raw contract JSON. After: a "Property Constraints (N)" section, placed after the settings as on iOS, lists each rule:

PROPERTY CONSTRAINTS (2)
  perUnitFee
    {
        "greaterThanOrEqual": [
            {
                "divide": [
                    "price",
                    "fee"
                ]
            },
            1
        ]
    }
    Reads: price (value), fee (value)

  sellerIsOwner                                           $ownerId
    { "anyOf": [ ... ] }            (indented the same way, scrolls sideways)
    Reads: sellerId (presence), sellerId (identifier)
    Reads $ownerId, the document's owner: transfers and purchases are judged against this rule too.

  Every created or replaced document must meet each rule, checked in name order. A document
  breaking one is refused (error 10422) and the fee is still charged.

The section is re-read when the SDK, its protocol version or the stored contract changes. A schema declaring rules that the current protocol version does not enforce says so; with no SDK, no stored contract bytes or an older native library, the section says why the rules cannot be read. Rows carry the iOS accessibility identifiers as test tags (documentType.propertyConstraint.<name>).

Create document (CreateDocumentScreen.kt). Before: an offer with {"price": 100, "fee": 0} was broadcast, and consensus refused it with DocumentPropertyConstraintViolatedError (10422) after charging the fee. After: "Create / Broadcast" runs the pre-check first and nothing is sent:

Not sent: a property constraint is broken
Rule: perUnitFee
Violation: DivisionByZero
Reason: it divides by zero

A document meeting every rule, or of a type declaring none, is broadcast as before. When the pre-check cannot run (no SDK, no stored contract bytes, the check itself fails, or the native library predates the exports), it logs a warning and the create proceeds, since consensus judges the document either way. As on iOS, only the create flow is pre-checked; replace and transfer (DocumentActionsScreen.kt) are unchanged, because the C function judges a document to create.

How Has This Been Tested?

  • Kotlin SDK, new sdk/src/test/.../queries/DocumentPropertyConstraintsTest.kt (13 JVM tests, no native library): the rules decode in order with their reads and readsOwner; each rule is kept as its declared JSON (compact, sorted keys, operand order untouched) and pretty-printed to the same JSON; [] decodes to an empty list; unknown read kinds and violation names are kept as Other(name); every known name round trips; a violation decodes with its description; null means every rule holds; malformed payloads (not JSON, not an array, a missing field, readsOwner as a number or a string, a non-string name) throw SerializationError. Same cases as the Swift SDK's DocumentPropertyConstraintsTests.
  • KotlinExampleApp, new app/src/test/.../ui/contracts/PropertyConstraintsTest.kt (13 JVM tests): the keyword check; the details section (hidden without asking Rust when no rules are declared, the rules read from the stored bytes, "not enforced" when Rust reads none, and why nothing could be read with no SDK, no bytes or a failed read); the create pre-check (passes without asking Rust when no rules are declared, stops a broken document, passes a valid one, lets the create proceed with no SDK, no bytes or a failed check, and rethrows a cancellation); a native library without the exports neither crashes nor blocks; the reads line and the alert text.
  • Instrumented, new sdk/src/androidTest/.../PropertyConstraintsFfiTest.kt (3 tests, run by CI's connectedDebugAndroidTest): both exports round trip on a trusted testnet SDK handle with the Swift test's contract fixture ([] and null), the error codes come through (NotFound 7, SerializationError 4, InvalidParameter 1), and an owner id of 20 bytes is refused. Compiled locally, not run (no emulator, and the .so was not rebuilt).
  • cargo check -p rs-unified-sdk-jni: clean. cargo clippy -p rs-unified-sdk-jni --all-targets -- -D warnings: clean. cargo test -p rs-unified-sdk-jni: 43 passed.
  • ./gradlew :sdk:compileDebugKotlin :app:compileDebugKotlin :sdk:compileDebugAndroidTestKotlin: succeeded (JDK: Android Studio's bundled JBR).
  • ./gradlew :sdk:testDebugUnitTest :app:testDebugUnitTest: 501 SDK tests and 232 app tests passed (the new ones included), 0 failures.
  • Not run: build_android.sh (the native cross-compile), so the app was not run on an emulator against the new exports; the rules section and the refusal alert were not exercised on a device.

Breaking Changes

None. New JNI exports, new Kotlin API and new app UI; existing calls are unchanged. The native library has to be rebuilt to carry the new exports (see Risks).

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

PR Hygiene · 5a56624

  • Bots — coderabbitai ✓ · thepastaclaw not yet — /skip-bots proceeds without the ones not yet reported
  • Self-review — post /self-reviewed once the bots are done
  • Within your 5 open PRs — this one is beyond the limit; it waits until one merges
  • Build green
  • Approvals
    • kotlin-sdk (packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/contracts/CreateDocumentScreen.kt, packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/contracts/DocumentTypeDetailsScreen.kt, packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/contracts/PropertyConstraints.kt and 6 more) — HashEngineering
    • files with no dedicated owner — you own it

When every box is checked the PR Hygiene check passes and this can merge.

Summary by CodeRabbit

  • New Features
    • Added APIs to retrieve document property constraints and check proposed document properties for violations, using the locally stored contract.
    • The example app now displays available constraints and checks documents before creation, showing an alert when a constraint is violated. If the check is unavailable, document creation can proceed.
  • Tests
    • Added coverage for constraint loading, validation outcomes, error handling, and displayed rule details.

QuantumExplorer and others added 2 commits September 27, 2026 19:20
…nd iOS example app

rs-sdk-ffi gains dash_sdk_data_contract_get_property_constraints (a
document type's rules as a JSON array, in name order, with what each
reads and whether it reads $ownerId) and
dash_sdk_data_contract_check_property_constraints (the first rule a
document to create breaks, or JSON null). Both take the contract as its
platform serialization, read at the SDK's protocol version, and match
wasm-dpp2's JSON shapes. The check builds the document the way
dash_sdk_document_create does (its parsing and building now shared as
two helpers) and judges it with DPP's validate_property_constraints.

The Swift SDK decodes both into DocumentPropertyConstraint and
PropertyConstraintViolation behind thin SDK wrappers, with
PersistentDocumentType helpers reading the stored contract bytes. The
example app lists the rules on the document type screen and refuses to
broadcast a document that breaks one, naming the rule and the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and Android example app

rs-unified-sdk-jni gains two QueriesNative exports,
dataContractGetPropertyConstraints and dataContractCheckPropertyConstraints,
each a thin marshaler over the rs-sdk-ffi function of the same purpose
(dash_sdk_data_contract_get_property_constraints and
dash_sdk_data_contract_check_property_constraints): the contract's platform
serialization, the document type, the properties JSON and the 32-byte owner
go in, the JSON string comes back, and FFI errors keep their codes.

The Kotlin SDK decodes both into DocumentPropertyConstraint,
PropertyConstraintRead and PropertyConstraintViolation (unknown read kinds and
violation names kept as Other(name)) behind sdk.contracts.propertyConstraints
and sdk.contracts.checkPropertyConstraints. No rule is evaluated in Kotlin.

The example app lists the rules on the document type screen and refuses to
broadcast a document that breaks one, naming the rule, the violation and the
reason. A pre-check that cannot run (no SDK, no stored contract bytes, a
native library without the exports) is logged and the create goes ahead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: dashpay/platform/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 6fd9bd8f-02d3-4220-a81b-0a1defe14c25

📥 Commits

Reviewing files that changed from the base of the PR and between a5a1af5 and 5a56624.

📒 Files selected for processing (10)
  • packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/contracts/CreateDocumentScreen.kt
  • packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/contracts/DocumentTypeDetailsScreen.kt
  • packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/contracts/PropertyConstraints.kt
  • packages/kotlin-sdk/KotlinExampleApp/app/src/test/java/org/dashfoundation/example/ui/contracts/PropertyConstraintsTest.kt
  • packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/PropertyConstraintsFfiTest.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/QueriesNative.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/queries/DocumentPropertyConstraints.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/queries/PlatformQueries.kt
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/queries/DocumentPropertyConstraintsTest.kt
  • packages/rs-unified-sdk-jni/src/queries.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The Kotlin SDK adds APIs to retrieve and check document property constraints through JNI. The Kotlin example app displays available constraints and checks them before document creation. A reported violation blocks creation; when the check cannot run, the app logs the reason and continues.

Changes

Document Property Constraints

Layer / File(s) Summary
Kotlin constraint queries and JNI forwarding
packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/queries/DocumentPropertyConstraints.kt, packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/queries/PlatformQueries.kt, packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/QueriesNative.kt, packages/rs-unified-sdk-jni/src/queries.rs, packages/kotlin-sdk/sdk/src/test/..., packages/kotlin-sdk/sdk/src/androidTest/...
Adds Kotlin models for rules and violations, query methods that return decoded results, and JNI exports that forward local queries to the FFI. JVM tests cover JSON decoding; Android tests cover native query results and error codes.
Kotlin example constraint display and create pre-check
packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/contracts/PropertyConstraints.kt, packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/contracts/DocumentTypeDetailsScreen.kt, packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/contracts/CreateDocumentScreen.kt, packages/kotlin-sdk/KotlinExampleApp/app/src/test/...
Loads and displays constraint rules and their read metadata. Before creating a document, the app checks constraints when the required SDK and contract data are available. A violation stops creation; a skipped check logs its reason and allows creation to proceed. JVM tests cover the app-side states and outcomes.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant CreateDocumentScreen
  participant Contracts
  participant QueriesNative
  participant JNI
  participant RustFFI
  CreateDocumentScreen->>Contracts: checkPropertyConstraints
  Contracts->>QueriesNative: dataContractCheckPropertyConstraints
  QueriesNative->>JNI: Send contract, properties, and owner ID
  JNI->>RustFFI: Forward constraint check
  RustFFI-->>CreateDocumentScreen: Return violation or no violation
Loading

Merge Risk: ⚪ Minimal · up to 5a566

No confirmed issue currently prevents merging. Native cross-compilation and device testing remain unrun.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 5a566

The new check can prevent some fee-bearing rejected submissions, while document creation remains subject to the existing network validation. The added native input path and behavior when the check is unavailable warrant review, but no introduced security bypass was established.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — Callers able to invoke the SDK methods can now send caller-held contract bytes and document properties into native parsing and evaluation. The evidenced exposure is within the caller process; no additional tenant, service, credential, or data-store authority was identified.

Trust Boundaries and Controls

  • observed — JNI copies and validates incoming byte arrays, requires exactly 32 owner bytes, and keeps those buffers alive through the synchronous native call. The native function checks required pointers and parses the properties and contract before evaluating the rules.

Resilience and Maintainability Implications

  • inferred — A skipped check can still lead to the same fee-bearing rejection possible before this PR; it is not an enforcement bypass introduced by the new path. The evidence does not establish that the locally checked contract snapshot always matches the contract version used for eventual submission.

Hardening Proposals

  • proposed — If the pre-check is intended to give users a dependable fee warning, make skipped checks visible at submission and verify that the checked contract version matches the one used by creation. This is a hardening proposal, not an established mismatch or security finding.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 66 functions across 10 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding property-constraint rules and a document pre-check to the Kotlin SDK and Android example app.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@thepastaclaw

thepastaclaw commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

🔍 Review in progress — actively reviewing now (commit 5a56624) · triage: normal

Base automatically changed from claude/property-constraints-swift to v4.2-dev September 27, 2026 16:03
@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 27, 2026
@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Sep 27, 2026
#5064 merged as a5a1af5. The only conflict was the add/add of
rs-sdk-ffi's property_constraints.rs, resolved to v4.2-dev (the squash
tree equals the final #5064 head a5fac13, including the pre-v14
fixture fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer merged commit 8783489 into v4.2-dev Sep 27, 2026
21 of 22 checks passed
@QuantumExplorer
QuantumExplorer deleted the claude/property-constraints-kotlin branch September 27, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-bots Waiting for the review bots to report on this head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants