Skip to content

feat(sdk)!: key limits on every client: wasm-dpp2, platform-wallet, FFI, Kotlin and Swift - #4811

Merged
QuantumExplorer merged 7 commits into
v4.2-devfrom
feat/key-limits-clients
Sep 18, 2026
Merged

QuantumExplorer merged 7 commits into
v4.2-devfrom
feat/key-limits-clients

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Sep 18, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Protocol version 14 gave authentication keys a budget and an expiry (#4798), a query for what is left of a budget (#4802) and the IdentityKeyLimitsUpdate transition that raises them (#4807). After those three, only rs-sdk, wasm-sdk and js-evo-sdk could top a key up, nobody outside Rust could register a key with limits (the wasm-dpp2 in-creation wrapper rebuilt a version 0 key and dropped them), and platform-wallet, the FFI layer, the Kotlin SDK and the Swift SDK carried nothing: a limited key crossed the FFI as an unlimited version 0 key and came back that way from persistence.

This PR carries key limits through every client.

What was done

wasm-dpp2 and the JS SDKs (c5ac0a3)

  • IdentityPublicKeyInCreation and IdentityPublicKey take totalBudget and expiresAt (bigint) in their options, expose them as getters and setters, and build a version 1 key when either is given; a key without limits stays a version 0 key with the same bytes as ever. The in-creation to key conversion goes through the dpp enum, so the limits follow the key into identityUpdate and sdk.identities.update.
  • The TS Object and JSON shapes list $formatVersion and the two limits. Specs cover the constructor, getters and setters, Object and JSON round trips for both key versions, and the conversion into an identity public key.
  • The evo-sdk guide gains "Register a key with a budget or an expiry"; the key limits chapter no longer calls limits immutable and names every client entry point.

platform-wallet, wallet-ffi, rs-sdk-ffi, JNI (7d52ca6)

  • IdentityWallet::update_identity_key_limits_with_external_signer computes the absolute values from the key the wallet holds (through rs-sdk's raised_key_limits), refuses what Platform would refuse and charge for, signs through the external signer, and lays the key as stored over the cached identity with the new ManagedIdentity::replace_key, so the client's key row follows through the persister. No identity revision is claimed or bumped.
  • Wallet signers prefer a key without limits and skip an expired one (usable_authentication_key, used for profile and contact-info writes); the FFI signing-key picker does the same.
  • Every key row that crosses the FFI carries total_budget and expires_at: registration and update rows (IdentityPubkeyFFI, registering a version 1 key when set), the managed identity's key snapshot (IdentityPublicKeyFFI), the persisted key entry (IdentityKeyEntryFFI, now 216 bytes) and the cold-restore row (IdentityKeyRestoreFFI), so a limited key persists and restores as limited.
  • New exports: platform_wallet_update_identity_key_limits_with_signer and dash_sdk_identity_fetch_keys_remaining_budgets (JSON {"5": "1000", "6": null}).
  • JNI: the pubkey row blob ends every row with a limits flags byte followed by the limits that are set (the golden registration fixture is regenerated as v2); the persisted-key upsert passes the four limit fields (...ZJZJ)I); restored rows read them; updateIdentityKeyLimits and identityFetchKeysRemainingBudgets natives.
  • rs-drive: the unused DocumentPropertyType import in the query module is gated like its only user, so the verify-only build the wallet crates use lints clean.

Kotlin SDK (241a868)

  • IdentityPubkey.totalBudget / expiresAt, codec writes the limits section, PublicKeyEntity gains the two nullable columns (Room schema 12, migration 11 to 12), the persistence handler stores them from the upsert callback and hands them back on cold restore.
  • IdentityUpdates.updateKeyLimits and Identities.fetchKeysRemainingBudgets.
  • IdentityPubkeyCodecTest and the 11 to 12 migration test.

Swift SDK (4th commit)

  • IdentityPublicKey, IdentityPublicKeyInfo and the IdentityPubkey registration row carry totalBudget / expiresAt; the one IdentityPubkeyFFI build site sets the four new C fields.
  • PersistentPublicKey stores both limits: SwiftData schema V5 with a lightweight V4 to V5 stage. V4's model graph is frozen (36 generated files; its relationships need the whole graph, unlike the single-model V3 freeze) and the freeze script pins it at 787cac0, which is on v4.2-dev. The persist-identity-keys callback reads the limits off IdentityKeyEntryFFI and writes them on every upsert; the cold-restore builder sets them on IdentityKeyRestoreFFI.
  • ManagedPlatformWallet.updateIdentityKeyLimits(identityId:keyId:addBudget:expiresAt:signer:) and SDK.fetchKeysRemainingBudgets(identityId:keyIds:) -> [UInt32: UInt64?].
  • The example app's hand-rolled JSON key builders read the two limits, so a fetched version 1 key is no longer persisted as unlimited.
  • Two existing migration tests that migrate to V4 now read back through DashSchemaV4.*, as the V3 test already does, since V4 is no longer the live schema.

How Has This Been Tested?

  • wasm-dpp2: yarn workspace @dashevo/wasm-dpp2 build, then mocha on PublicKeyInCreation, IdentityPublicKey, IdentityKeyLimitsUpdateTransition and IdentityUpdateTransition specs: all pass. cargo check for wasm-dpp2 and wasm-sdk on wasm32; clippy on wasm-dpp2.
  • Rust: cargo check --lib --tests and cargo clippy --all-targets -- -D warnings for platform-wallet, platform-wallet-ffi, rs-unified-sdk-jni, rs-sdk-ffi; tests: JNI pubkey_rows (23, including the v2 golden fixture), platform-wallet key_selection (11), wallet-ffi row helpers (40). cargo check -p drive --no-default-features --features verify (the old warning is gone) and cargo clippy -p drive --all-features.
  • Kotlin: not compiled locally (no JDK on the machine); the Kotlin CI job is the check. The Room schema export sdk/schemas/.../12.json is produced by the first Gradle build and needs to be committed then.
  • Swift: build_ios.sh --target tests --profile dev; swift test (full: 584 tests, 15 skipped, 0 failures), including the new IdentityKeyLimitsPersistenceTests (6) and DashModelMigrationTests (14, with the V4 to V5 migration and its dash-v5.store fixture); freeze_schema_models.py --check (73 frozen files match); SwiftExampleApp xcodebuild test (unit tests) and a warnings-as-errors build. Not exercised against a node: the two new calls are compile-verified and their marshalling reviewed against the generated headers; the cold-restore limits path is compile-verified only (no unit-test seam).

Breaking Changes

No consensus change. FFI ABI: IdentityPubkeyFFI, IdentityPublicKeyFFI, IdentityKeyEntryFFI (184 to 216 bytes) and IdentityKeyRestoreFFI gained four trailing fields; the JNI pubkey row blob gained a trailing limits flags byte per row and the persisted-key upsert descriptor gained ZJZJ; Room schema 11 to 12 with a migration; SwiftData schema V5 with the Swift commit.

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

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

QuantumExplorer and others added 3 commits September 18, 2026 18:03
…nd the JS SDKs

`IdentityPublicKeyInCreation` and `IdentityPublicKey` in wasm-dpp2 take
`totalBudget` and `expiresAt` (bigint) in their options, expose them as
getters and setters, and build a version 1 key when either is given; a
key without limits stays a version 0 key with the same bytes as ever.
The in-creation to key conversion goes through the dpp enum so the
limits follow the key into `identityUpdate`, which previously rebuilt a
version 0 key and dropped them. The TS Object and JSON shapes list
`$formatVersion` and the two limits.

Specs cover the constructor, the getters and setters, Object and JSON
round trips for both key versions, and the conversion into an identity
public key. The evo-sdk guide shows registering an app key with a
budget, and the key limits chapter no longer calls limits immutable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and the JNI

The wallet raises the limits of one of an identity's keys:
`update_identity_key_limits_with_external_signer` computes the absolute
values from the key it holds, refuses what Platform would charge for,
signs through the external signer, and lays the key as stored over the
cached identity with a new `ManagedIdentity::replace_key`, so the
client's key row follows through the persister. Its signers prefer a
key without limits and skip an expired one.

Every key row that crosses the FFI carries `total_budget` and
`expires_at`: the registration and update rows (`IdentityPubkeyFFI`,
registering a version 1 key when set), the managed identity's key
snapshot, the persisted key entry (`IdentityKeyEntryFFI`, now 216 bytes)
and the cold-restore row, so a limited key persists and restores as
limited. New exports: `platform_wallet_update_identity_key_limits_with_signer`
and `dash_sdk_identity_fetch_keys_remaining_budgets`; the FFI signing-key
picker skips expired keys and prefers unlimited ones.

The JNI row blob ends every row with a limits flags byte followed by the
limits that are set (the golden registration fixture is regenerated as
v2), the persisted key upsert passes the four limit fields, restored
rows read them, and `updateIdentityKeyLimits` and
`identityFetchKeysRemainingBudgets` natives are added.

The unused `DocumentPropertyType` import in rs-drive's query module is
gated like its only user, so the verify-only build of drive, which the
wallet crates use, lints clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…top up, read what is left

`IdentityPubkey` carries `totalBudget` and `expiresAt`, and the row codec
ends every row with a limits flags byte followed by the limits that are
set, matching the JNI parser; the golden registration fixture moves to
v2 with that byte. `PublicKeyEntity` gains the two nullable columns
(Room schema 12, migration 11 to 12), the persistence handler stores
them from the upsert callback and hands them back on cold restore, so a
limited key persists and restores as limited.

`IdentityUpdates.updateKeyLimits` adds credits to a key's budget or
moves its expiry later through the new native, and
`Identities.fetchKeysRemainingBudgets` reads what is left of the
budgets as JSON. Unit tests cover the codec's limits section; the
migration test covers 11 to 12. The Room schema export for version 12
is produced by the next Gradle build.

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

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Important

Review skipped

Too many files!

This PR contains 104 files, which is 4 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: d1422997-16f3-4c54-a90d-7a5acb9bafc8

📥 Commits

Reviewing files that changed from the base of the PR and between 3250d53 and 60f38bd.

⛔ Files ignored due to path filters (2)
  • packages/kotlin-sdk/sdk/src/test/resources/golden/registration_pubkeys_v1.bin is excluded by !**/*.bin
  • packages/kotlin-sdk/sdk/src/test/resources/golden/registration_pubkeys_v2.bin is excluded by !**/*.bin
📒 Files selected for processing (104)
  • book/src/data-model/key-limits.md
  • book/src/evo-sdk/state-transitions.md
  • packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseMigrationTest.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/QueriesNative.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/TransactionsNative.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityPubkeyCodec.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityUpdates.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabase.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/entities/PublicKeyEntity.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/queries/PlatformQueries.kt
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/identity/IdentityPubkeyCodecTest.kt
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/identity/RegistrationKeysTest.kt
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseTest.kt
  • packages/rs-drive/src/query/mod.rs
  • packages/rs-platform-wallet-ffi/src/identity_key_limits.rs
  • packages/rs-platform-wallet-ffi/src/identity_persistence.rs
  • packages/rs-platform-wallet-ffi/src/identity_registration_with_signer.rs
  • packages/rs-platform-wallet-ffi/src/identity_update.rs
  • packages/rs-platform-wallet-ffi/src/invitation.rs
  • packages/rs-platform-wallet-ffi/src/lib.rs
  • packages/rs-platform-wallet-ffi/src/managed_identity.rs
  • packages/rs-platform-wallet-ffi/src/persistence.rs
  • packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/contact_info.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/key_limits.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/mod.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/profile.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/update.rs
  • packages/rs-platform-wallet/src/wallet/identity/state/managed_identity/identity_ops.rs
  • packages/rs-sdk-ffi/src/identity/keys.rs
  • packages/rs-sdk-ffi/src/identity/mod.rs
  • packages/rs-sdk-ffi/src/identity/queries/keys_remaining_budgets.rs
  • packages/rs-sdk-ffi/src/identity/queries/mod.rs
  • packages/rs-unified-sdk-jni/src/persistence.rs
  • packages/rs-unified-sdk-jni/src/pubkey_rows.rs
  • packages/rs-unified-sdk-jni/src/queries.rs
  • packages/rs-unified-sdk-jni/src/transactions.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/DPP/DPPIdentity.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/FFI/PlatformQueryExtensions.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentAccount.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentAssetLock.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentCoreAddress.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDPNSName.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDashpayContactProfile.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDashpayContactRequest.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDashpayIgnoredSender.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDashpayPayment.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDashpayProfile.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDataContract.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDocument.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDocumentType.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentIdentity.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentIndex.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentInvitation.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentKeyword.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentMasternode.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentPendingInput.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentPlatformAddress.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentPlatformAddressesSyncState.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentProperty.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentPublicKey.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentShieldedActivity.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentShieldedNote.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentShieldedOutgoingNote.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentShieldedSyncState.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentShieldedViewingKey.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentToken.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentTokenBalance.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentTokenHistoryEvent.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentTrackedMasternode.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentTransaction.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentTxo.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentWallet.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentWalletManagerMetadata.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+TokenTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentPublicKey.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedIdentity.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/README.md
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/IdentityKeyRefresher.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/JSONSafeInteger.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/TokenDirectPurchasePricing.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DiagnosticsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentWithPriceView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/LoadIdentityView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TokenDetailsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/JSONSafeIntegerTests.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/TokenDirectPurchasePricingTests.swift
  • packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift
  • packages/swift-sdk/SwiftTests/SwiftDashSDKTests/Fixtures/SchemaStores/dash-v5.store
  • packages/swift-sdk/SwiftTests/SwiftDashSDKTests/IdentityKeyLimitsPersistenceTests.swift
  • packages/swift-sdk/scripts/freeze_schema_models.py
  • packages/swift-sdk/scripts/test_freeze_schema_models.py
  • packages/wasm-dpp2/src/identity/public_key.rs
  • packages/wasm-dpp2/src/identity/transitions/public_key_in_creation.rs
  • packages/wasm-dpp2/tests/unit/IdentityPublicKey.spec.ts
  • packages/wasm-dpp2/tests/unit/PublicKeyInCreation.spec.ts
  • packages/wasm-sdk/src/state_transitions/identity.rs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing Touches
📝 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.

@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

PR Hygiene

State: waiting-bots · commit 60f38bd3e00cc4235bdc662a75381881ea5a6da9

  • coderabbitai has not reported for the current head
  • thepastaclaw has not reported for the current head

Self-review is an author attestation that you have read the diff:
/self-reviewed — covers everything pushed so far; post it again after a new push.

This report does not bypass CI or repository protection rules.

@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 18, 2026
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

📖 Book Preview built successfully.

Download the preview from the workflow artifacts.
To view locally: download the artifact, unzip, and open index.html.

Updated at 2026-09-18T12:34:13.670Z

…up, read what is left

`IdentityPublicKey`, `IdentityPublicKeyInfo` and the `IdentityPubkey`
registration row carry `totalBudget` and `expiresAt`, and the one place
that builds `IdentityPubkeyFFI` sets the four new C fields, so a key
registered from Swift may carry limits. `PersistentPublicKey` stores
both (SwiftData schema V5 with a lightweight V4 to V5 stage; V4's model
graph is frozen, 36 files, since its relationships need the whole graph),
the persist-identity-keys callback reads them off `IdentityKeyEntryFFI`
and writes them on every upsert, and the cold-restore builder sets them
on `IdentityKeyRestoreFFI`, so a limited key persists and restores as
limited.

`ManagedPlatformWallet.updateIdentityKeyLimits(identityId:keyId:addBudget:expiresAt:signer:)`
raises a key's limits through the wallet FFI and
`SDK.fetchKeysRemainingBudgets(identityId:keyIds:)` reads what is left
of the budgets. The example app's hand-rolled JSON key builders read
the two limits too, so a fetched version 1 key is no longer persisted
as unlimited. Tests: persistence of both limits, a version 0 key staying
unlimited, a raise overwriting, the Codable round trip, and the V4 to
V5 migration with its fixture store.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@thepastaclaw

thepastaclaw commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

⚠️ DEGRADED — Queued for automated review — 33rd in line, estimated start in ~17 h (commit 60f38bd)
Estimated review time once started: ~1.0 h (two-phase automated review; median of recent runs).
The primary review models are currently out of quota; this review will run on stand-in models and be marked as degraded.

  • Request priority review — click to move this review to the front of the queue.

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Found three issues in this revision, detailed inline. Validation: 34 targeted Rust/JNI tests, 19 Swift tests, and 5 schema-generator tests passed. Two temporary Rust regression tests reproduced the derivation-metadata and key-selection issues; original sources were restored. The Swift string-decoding issue was also reproduced. Android and WASM suites were not run.

Comment thread packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/LoadIdentityView.swift Outdated
@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.54%. Comparing base (9f81413) to head (60f38bd).
⚠️ Report is 6 commits behind head on v4.2-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4811      +/-   ##
============================================
- Coverage     78.13%   77.54%   -0.60%     
============================================
  Files          2962     2963       +1     
  Lines        423226   427917    +4691     
============================================
+ Hits         330703   331835    +1132     
- Misses        92523    96082    +3559     
Components Coverage Δ
dpp 75.88% <ø> (-1.11%) ⬇️
drive 78.48% <ø> (-0.47%) ⬇️
drive-abci 78.64% <ø> (-0.44%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 86.60% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 27.67% <ø> (+0.37%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

QuantumExplorer and others added 3 commits September 18, 2026 19:21
…er, string-encoded limits on iOS

Addresses the three review threads on #4811 and the Kotlin unit test
that pinned the Room schema at version 11.

platform-wallet:
- `ManagedIdentity::replace_key` emits the limits upsert without a
  derivation breadcrumb. The managed identity does not know per key
  whether it was wallet-derived, so coordinates built from
  `(wallet_id, identity_index, key_id)` were invented for an external
  key and would overwrite the linkage the client had persisted. Every
  persister keeps the breadcrumb it already holds when an upsert
  arrives without one. Pinned by `replace_key_carries_no_breadcrumb`.
- `usable_authentication_key` takes the target contract id and
  document type and skips a key whose bounds do not cover them: a
  single-contract bound must name the contract, a document-type bound
  must match both, and a contract group bound is never chosen offline
  because membership is state the wallet does not hold. Profile passes
  DashPay + `profile`, contact info passes DashPay + `contactInfo`.
  Pinned by the unbound-limited-key-over-bound-unlimited-key test and
  the accepted/rejected bounds tests.

swift-sdk (example app):
- `UInt64(jsonValue:)` reads a protocol u64 from a JSON number or the
  decimal string DPP writes above 2^53 - 1, and refuses booleans,
  negatives, fractions and out-of-range values. Both parsing branches
  of `LoadIdentityView` and `IdentityKeyRefresher` use it for
  `totalBudget`, `expiresAt` and `disabledAt`. Unit tests cover both
  shapes end to end through `JSONSerialization`.

kotlin-sdk:
- `DashDatabaseTest` now pins schema version 12 (11 -> 12 added the
  key limits columns).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ally uses

The instrumented smoke test `persistenceBridgeDescriptorsAllResolve`
resolves `BRIDGE_METHOD_TABLE` up front, and that table still listed the
pre-limits `onPersistIdentityKeyUpsert` descriptor while the trampoline
called the `...ZJZJ)I` one. Bind the descriptor to one constant used at
both sites so they cannot drift again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…or decimal string

DPP writes a u64 above 2^53 - 1 as a decimal string, so every bare
numeric cast in the example app dropped exactly the large values. Route
the remaining readers through `UInt64(jsonValue:)`:

- AppState query self-test summary: `balance` and a bare integer result.
- DiagnosticsView `formatTestResult`: the integer branch, same order.
- DocumentWithPriceView: the four-way NSNumber / String / Int / UInt64
  chain for `$price` collapsed to one call, which also refuses a
  negative or fractional price instead of coercing it.
- TokenDirectPurchasePricing: `single_price` and every tier's `amount`
  and `price` had no string fallback, so an expensive token was reported
  as not for direct sale. Tests cover string-encoded single prices, a
  tier schedule where each half crosses the ceiling, and unparseable
  strings.
- TokenDetailsView: `FixedAmount.amount` is a `TokenAmount` on a
  `json_safe_fields` type; `interval` is not and stays as is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer merged commit ba01d4c into v4.2-dev Sep 18, 2026
34 of 35 checks passed
@QuantumExplorer
QuantumExplorer deleted the feat/key-limits-clients branch September 18, 2026 13:04
QuantumExplorer added a commit that referenced this pull request Sep 18, 2026
…nd-trip

Android stored a key's bounds as an id plus an optional document type name
and inferred the kind on restore, so a key bound to a contract group (kind 3)
came back as a single-contract bound to the group id and the local signing
preflight refused valid batches until the identity was refreshed.

- public_keys gains a nullable contractBoundsKind column (schema 13, manual
  MIGRATION_12_13 in the style of the earlier bumps; 12 is the key usage
  limits schema from #4811, whose exported 12.json is committed here too).
  The persist callback records the kind the native row carries; restore
  honours it and keeps the old inference for legacy rows with a NULL kind.
  A stored kind this build does not know, or a stored 0 next to a stale
  blob, restores unbounded, the same fallback the Swift restore path
  applies; a kind 2 without its document type demotes to 1 as Rust does.
- ContractBounds.ContractGroup in the SDK type, kind 3 in IdentityPubkeyCodec
  (an id and no document type, ahead of the limits flags), kind docs updated
  in the bridges and the JNI blob layout.
- Tests: kind 3 and every other kind round-trip through the handler, legacy
  rows still infer 0, 1 and 2, unknown and zero kinds restore unbounded,
  the codec bytes for kind 3, the schema version and column, and a 12 to 13
  migration test (instrumented, compiled only).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
QuantumExplorer added a commit that referenced this pull request Sep 18, 2026
…d-trip

Swift stored a key's bounds as an id plus an optional document type name and
inferred the kind on restore, and the persist callback dropped kinds it did
not know. A key bound to a contract group (kind 3) therefore restored
unbounded after a restart.

- PersistentPublicKey gains an optional contractBoundsKind on the live
  schema version 5. V5 was cut by #4811 and has shipped in no tag, so the
  column joins it in place, the way the DashModelContainer doc allows before
  a version ships, and the dash-v5 fixture store is rewritten by this build:
  no new version and no new frozen copies. A nil kind marks a legacy row and
  keeps the old inference.
- The persist path stores the kind the FFI row carries, including 3; the
  restore path emits the stored kind (kind 3 with the id and no document
  type, a kind 2 row without its name demoted to 1 as Rust does, a kind this
  build does not know restored unbounded).
- ManagedPlatformWallet.ContractBounds gains contractGroup(id:), with the
  pin and parse paths for kind 3. The DPP-layer bounds enum is unchanged
  (generated frozen schemas switch over it exhaustively), so that projection
  reports no bounds for a group-bound row instead of a false single contract.
- The example app's storage inspector shows the bounds variant.
- Tests: kind 3 round trip, kinds 0 to 2 unchanged, legacy inference,
  unusable rows, pin and parse of kind 3, unknown kinds still rejected, the
  V4 to V5 backfill of the kind column, and the V4/V5 entity-set pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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