Repository navigation
swift-sdk: the DPP-layer ContractBounds cannot represent a contract-group bound the wallet layer reports #4853
Description
Expected Behavior
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.
ManagedPlatformWallet.ContractBounds
has three cases since #4800:
case singleContract(id: Data)
case singleContractDocumentType(id: Data, documentTypeName: String)
case contractGroup(id: Data) // kind == 3 on the FFI sideThe top-level ContractBounds
— the one IdentityPublicKey.contractBounds is typed as — still has two, and its Codable
BoundType enumerates the same two.
The SDK's own persistence seam already documents the consequence and chooses to drop the bound
(PersistentPublicKey.toIdentityPublicKey()):
A
ContractGroupbound (kind 3) has no variant on the DPP-layerContractBounds, and its id
is a group id, not a contract id, so projecting it as.singleContractwould 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.
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 → 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-ios develop: 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)onContractBounds, plus the matchingBoundTypecase and
Codablearms.- 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.
Steps to Reproduce (for bugs)
- Check out
v4.2-devat72b58f6073(any commit at or after fix(sdk): persist the contract bounds kind on Android and iOS #4800,5568dfa443). - In a Swift consumer of the SDK, exhaustively switch over a
ManagedPlatformWallet.IdentityPubkey'scontractBoundsand return a DPP-layer
ContractBoundsfrom each arm — the shapePlatformDashConnectDataSource.parse()uses. - Build. The switch is non-exhaustive, and
.contractGrouphas no arm that can be written.
Against the app: check out dashpay/dashwallet-ios develop 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-ios develop 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).
Your Environment
- Version used:
packages/swift-sdkonv4.2-devat72b58f6073; first broken at5568dfa443(fix(sdk): persist the contract bounds kind on Android and iOS #4800) - Environment name and version: Swift 6 / Xcode 26.5,
aarch64-apple-ios-sim, Rust 1.98.1 - Operating System and version: macOS (Darwin 25.6.0), iOS Simulator 26.5
- Link to your project: https://github.com/dashpay/dashwallet-ios at
65d6aa101(develop)
Activity
fix(dashpay): refuse a contract-group-bound key registration