You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A Swift SDK consumer that reads a key's contract bounds from ManagedPlatformWallet and
converts them to the DPP-layer IdentityPublicKey should be able to represent every bound the
wallet layer can report. Either the DPP-layer ContractBounds models a contract-group bound, or
the SDK offers a conversion whose lossiness is part of its signature, so a consumer cannot lose
the bound by accident.
Current Behavior
packages/swift-sdk carries two ContractBounds enums with different shapes.
case singleContract(id: Data)case singleContractDocumentType(id: Data, documentTypeName: String)case contractGroup(id: Data) // kind == 3 on the FFI side
The top-level ContractBounds
— the one IdentityPublicKey.contractBounds is typed as — still has two, and its Codable BoundType enumerates the same two.
A ContractGroup bound (kind 3) has no variant on the DPP-layer ContractBounds, and its id
is a group id, not a contract id, so projecting it as .singleContract would claim a bound the
key does not have. Report no bounds instead; the row keeps the group bound for the FFI restore
path, which does model it.
Inside the SDK this is deliberate and safe, because the group bound survives in the store for the
FFI restore path. For a consumer outside the SDK it is neither. A consumer that maps ManagedPlatformWallet.ContractBounds → ContractBounds gets a non-exhaustive switch with no
correct arm to add: .contractGroup has nowhere to go. Its choices are to drop the bound — which
is what the SDK does internally, but without the store to fall back on — or to refuse the value.
This is not hypothetical. It broke dashpay/dashwallet-iosdevelop: after the app's local SDK
pin moved onto current v4.2-dev, PlatformDashConnectDataSource.parse()
stopped compiling:
PlatformDashConnectDataSource.swift:246:33: error: switch must be exhaustive
note: add missing case: '.contractGroup(id: let id)'
Dropping the bound is not an option there. That mapping feeds the check that decides which
approved app a scanned key-registration belongs to; a key whose bound is silently erased "names
no single app", and the app also rebuilds an IdentityPubkey from the same value to hand back to updateIdentity(...), so a dropped bound would mean signing a weaker transition than the dApp
asked for. The app therefore has to refuse contract-group-bound keys outright.
Possible Solution
Root cause: #4800 extended the wallet-layer enum to carry FFI kind 3, but the DPP-layer enum it
is routinely converted into was left at two cases, and nothing in the SDK's public surface marks
the conversion as lossy.
Preferred fix — add the variant to the DPP-layer enum:
case contractGroup(id: Identifier) on ContractBounds, plus the matching BoundType case and Codable arms.
Update PersistentPublicKey.toIdentityPublicKey() / .from(_:identityId:) to carry kind 3
instead of flattening it to "no bounds", so a SwiftData round-trip stops weakening the bound.
Audit description (CustomStringConvertible) and any other exhaustive switch over the enum.
This is source-breaking for anything switching over ContractBounds, so it wants the same feat(sdk)! treatment #4800 and #4811 had.
Alternative, if the DPP-layer enum must stay pinned to what rs-dpp models: expose an explicit
converter (e.g. ManagedPlatformWallet.ContractBounds.asDPPBounds() throws -> ContractBounds)
whose signature makes the failure visible, and say in the doc comment that kind 3 has no DPP
representation. That keeps the enums as they are but stops each consumer from re-deriving the
same lossy decision on its own.
This needs a maintainer's call: whether the DPP-layer enum is meant to mirror rs-dpp exactly, or
to be the SDK's Swift-facing model of a key's bounds.
In a Swift consumer of the SDK, exhaustively switch over a ManagedPlatformWallet.IdentityPubkey's contractBounds and return a DPP-layer ContractBounds from each arm — the shape PlatformDashConnectDataSource.parse() uses.
Build. The switch is non-exhaustive, and .contractGroup has no arm that can be written.
Against the app: check out dashpay/dashwallet-iosdevelop at 65d6aa101, point the local
package at packages/swift-sdk on v4.2-dev at 72b58f6073, rebuild DashSDKFFI.xcframework with ./build_ios.sh --target sim, and build the dashpay scheme.
Context
Blocks building dashpay/dashwallet-iosdevelop against current v4.2-dev. The app-side
workaround — refusing a contract-group-bound key registration — is in dashwallet-ios#1138, but
it is a refusal, not support: DashConnect cannot accept such a key until the SDK can represent
the bound end to end. The same trap is waiting for any other consumer that converts between the
two enums.
Related: #4800 (added the wallet-layer variant), #4791 / #4793 (contract groups), #4811 (key
limits, the other source-breaking SDK change in the same window).
Expected Behavior
A Swift SDK consumer that reads a key's contract bounds from
ManagedPlatformWalletandconverts them to the DPP-layer
IdentityPublicKeyshould be able to represent every bound thewallet layer can report. Either the DPP-layer
ContractBoundsmodels a contract-group bound, orthe SDK offers a conversion whose lossiness is part of its signature, so a consumer cannot lose
the bound by accident.
Current Behavior
packages/swift-sdkcarries twoContractBoundsenums with different shapes.ManagedPlatformWallet.ContractBoundshas three cases since #4800:
The top-level
ContractBounds— the one
IdentityPublicKey.contractBoundsis typed as — still has two, and itsCodableBoundTypeenumerates the same two.The SDK's own persistence seam already documents the consequence and chooses to drop the bound
(
PersistentPublicKey.toIdentityPublicKey()):and the reverse conversion
(
PersistentPublicKey.from(_:identityId:))notes that it "only ever writes kinds 0, 1 and 2".
Inside the SDK this is deliberate and safe, because the group bound survives in the store for the
FFI restore path. For a consumer outside the SDK it is neither. A consumer that maps
ManagedPlatformWallet.ContractBounds→ContractBoundsgets a non-exhaustiveswitchwith nocorrect arm to add:
.contractGrouphas nowhere to go. Its choices are to drop the bound — whichis what the SDK does internally, but without the store to fall back on — or to refuse the value.
This is not hypothetical. It broke
dashpay/dashwallet-iosdevelop: after the app's local SDKpin moved onto current
v4.2-dev,PlatformDashConnectDataSource.parse()stopped compiling:
Dropping the bound is not an option there. That mapping feeds the check that decides which
approved app a scanned key-registration belongs to; a key whose bound is silently erased "names
no single app", and the app also rebuilds an
IdentityPubkeyfrom the same value to hand back toupdateIdentity(...), so a dropped bound would mean signing a weaker transition than the dAppasked for. The app therefore has to refuse contract-group-bound keys outright.
Possible Solution
Root cause: #4800 extended the wallet-layer enum to carry FFI kind 3, but the DPP-layer enum it
is routinely converted into was left at two cases, and nothing in the SDK's public surface marks
the conversion as lossy.
Preferred fix — add the variant to the DPP-layer enum:
case contractGroup(id: Identifier)onContractBounds, plus the matchingBoundTypecase andCodablearms.PersistentPublicKey.toIdentityPublicKey()/.from(_:identityId:)to carry kind 3instead of flattening it to "no bounds", so a SwiftData round-trip stops weakening the bound.
description(CustomStringConvertible) and any other exhaustive switch over the enum.This is source-breaking for anything switching over
ContractBounds, so it wants the samefeat(sdk)!treatment #4800 and #4811 had.Alternative, if the DPP-layer enum must stay pinned to what
rs-dppmodels: expose an explicitconverter (e.g.
ManagedPlatformWallet.ContractBounds.asDPPBounds() throws -> ContractBounds)whose signature makes the failure visible, and say in the doc comment that kind 3 has no DPP
representation. That keeps the enums as they are but stops each consumer from re-deriving the
same lossy decision on its own.
This needs a maintainer's call: whether the DPP-layer enum is meant to mirror
rs-dppexactly, orto be the SDK's Swift-facing model of a key's bounds.
Steps to Reproduce (for bugs)
v4.2-devat72b58f6073(any commit at or after fix(sdk): persist the contract bounds kind on Android and iOS #4800,5568dfa443).ManagedPlatformWallet.IdentityPubkey'scontractBoundsand return a DPP-layerContractBoundsfrom each arm — the shapePlatformDashConnectDataSource.parse()uses..contractGrouphas no arm that can be written.Against the app: check out
dashpay/dashwallet-iosdevelopat65d6aa101, point the localpackage at
packages/swift-sdkonv4.2-devat72b58f6073, rebuildDashSDKFFI.xcframeworkwith./build_ios.sh --target sim, and build thedashpayscheme.Context
Blocks building
dashpay/dashwallet-iosdevelopagainst currentv4.2-dev. The app-sideworkaround — refusing a contract-group-bound key registration — is in
dashwallet-ios#1138, butit is a refusal, not support: DashConnect cannot accept such a key until the SDK can represent
the bound end to end. The same trap is waiting for any other consumer that converts between the
two enums.
Related: #4800 (added the wallet-layer variant), #4791 / #4793 (contract groups), #4811 (key
limits, the other source-breaking SDK change in the same window).
Your Environment
packages/swift-sdkonv4.2-devat72b58f6073; first broken at5568dfa443(fix(sdk): persist the contract bounds kind on Android and iOS #4800)aarch64-apple-ios-sim, Rust 1.98.165d6aa101(develop)