feat(sdk)!: key limits on every client: wasm-dpp2, platform-wallet, FFI, Kotlin and Swift - #4811
Conversation
…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>
|
Important Review skippedToo 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (104)
You can disable this status message by setting the ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
PR HygieneState: waiting-bots · commit
Self-review is an author attestation that you have read the diff: This report does not bypass CI or repository protection rules. |
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. 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>
|
|
QuantumExplorer
left a comment
There was a problem hiding this comment.
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 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
🚀 New features to boost your workflow:
|
…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>
…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>
…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>
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
IdentityKeyLimitsUpdatetransition that raises them (#4807). After those three, onlyrs-sdk,wasm-sdkandjs-evo-sdkcould 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), andplatform-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)
IdentityPublicKeyInCreationandIdentityPublicKeytaketotalBudgetandexpiresAt(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 intoidentityUpdateandsdk.identities.update.$formatVersionand 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.platform-wallet, wallet-ffi, rs-sdk-ffi, JNI (7d52ca6)
IdentityWallet::update_identity_key_limits_with_external_signercomputes the absolute values from the key the wallet holds (through rs-sdk'sraised_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 newManagedIdentity::replace_key, so the client's key row follows through the persister. No identity revision is claimed or bumped.usable_authentication_key, used for profile and contact-info writes); the FFI signing-key picker does the same.total_budgetandexpires_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.platform_wallet_update_identity_key_limits_with_signeranddash_sdk_identity_fetch_keys_remaining_budgets(JSON{"5": "1000", "6": null})....ZJZJ)I); restored rows read them;updateIdentityKeyLimitsandidentityFetchKeysRemainingBudgetsnatives.DocumentPropertyTypeimport 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,PublicKeyEntitygains 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.updateKeyLimitsandIdentities.fetchKeysRemainingBudgets.IdentityPubkeyCodecTestand the 11 to 12 migration test.Swift SDK (4th commit)
IdentityPublicKey,IdentityPublicKeyInfoand theIdentityPubkeyregistration row carrytotalBudget/expiresAt; the oneIdentityPubkeyFFIbuild site sets the four new C fields.PersistentPublicKeystores 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 offIdentityKeyEntryFFIand writes them on every upsert; the cold-restore builder sets them onIdentityKeyRestoreFFI.ManagedPlatformWallet.updateIdentityKeyLimits(identityId:keyId:addBudget:expiresAt:signer:)andSDK.fetchKeysRemainingBudgets(identityId:keyIds:) -> [UInt32: UInt64?].DashSchemaV4.*, as the V3 test already does, since V4 is no longer the live schema.How Has This Been Tested?
yarn workspace @dashevo/wasm-dpp2 build, then mocha onPublicKeyInCreation,IdentityPublicKey,IdentityKeyLimitsUpdateTransitionandIdentityUpdateTransitionspecs: all pass.cargo checkfor wasm-dpp2 and wasm-sdk on wasm32; clippy on wasm-dpp2.cargo check --lib --testsandcargo clippy --all-targets -- -D warningsforplatform-wallet,platform-wallet-ffi,rs-unified-sdk-jni,rs-sdk-ffi; tests: JNIpubkey_rows(23, including the v2 golden fixture), platform-walletkey_selection(11), wallet-ffi row helpers (40).cargo check -p drive --no-default-features --features verify(the old warning is gone) andcargo clippy -p drive --all-features.sdk/schemas/.../12.jsonis produced by the first Gradle build and needs to be committed then.build_ios.sh --target tests --profile dev;swift test(full: 584 tests, 15 skipped, 0 failures), including the newIdentityKeyLimitsPersistenceTests(6) andDashModelMigrationTests(14, with the V4 to V5 migration and itsdash-v5.storefixture);freeze_schema_models.py --check(73 frozen files match); SwiftExampleAppxcodebuild 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) andIdentityKeyRestoreFFIgained four trailing fields; the JNI pubkey row blob gained a trailing limits flags byte per row and the persisted-key upsert descriptor gainedZJZJ; Room schema 11 to 12 with a migration; SwiftData schema V5 with the Swift commit.Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code