From 3f79c7afd0a0f309a9805edc69559a4d2d5690d9 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 8 Sep 2026 18:30:52 +0700 Subject: [PATCH 01/12] feat(swift-sdk): generate the frozen SwiftData schema models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A released `DashSchemaVN` identifies a store by the checksum of the entities it declares, so the models it registers must never change shape again. Those shapes lived in one hand-written DashSchemaFrozenModels.swift: nothing proved a copy equalled the shape that shipped, and nothing stopped an edit to it. `scripts/freeze_schema_models.py` now generates every frozen copy from the live model sources at the commit that last had the released shape, one file per model under Persistence/FrozenSchemas/. `FREEZES` in the script is the append-only record of which commit each version's shapes come from, and `--check` fails if any frozen file on disk differs from what that record produces, so a hand edit cannot survive review. Every model V1–V3 register is frozen, not only the ones whose shape has since changed: a relationship binds its destination by entity name, and SwiftData resolves that name to whichever Swift type claimed it first in the process, so a frozen model reached from a live one could be hashed with the live shape. The V1/V2 asset lock is generated from the last commit before `recipientIsExternal` was added; the `TokenTypes` value types `PersistentToken` stores inline are frozen alongside it because SwiftData expands them into the entity's columns. The hand-written file is deleted. A standalone SwiftData probe built once from it and once from the generated files writes identical per-entity hashes and checksums for V1 (34 entities), V2 (35) and V3 (35), with the live schema built first and with it built last, so no existing store changes which migration source it matches. V4, the live schema, is untouched. Co-Authored-By: Claude Fable 5.1 --- .../Persistence/DashModelContainer.swift | 196 +- .../Persistence/DashSchemaFrozenModels.swift | 3384 ----------------- .../DashSchemaV1+PersistentAccount.swift | 78 + .../DashSchemaV1+PersistentAssetLock.swift | 100 + .../DashSchemaV1+PersistentCoreAddress.swift | 68 + .../DashSchemaV1+PersistentDPNSName.swift | 132 + ...maV1+PersistentDashpayContactProfile.swift | 97 + ...maV1+PersistentDashpayContactRequest.swift | 118 + ...emaV1+PersistentDashpayIgnoredSender.swift | 67 + ...ashSchemaV1+PersistentDashpayPayment.swift | 99 + ...ashSchemaV1+PersistentDashpayProfile.swift | 70 + .../DashSchemaV1+PersistentDataContract.swift | 286 ++ .../DashSchemaV1+PersistentDocument.swift | 181 + .../DashSchemaV1+PersistentDocumentType.swift | 101 + .../DashSchemaV1+PersistentIdentity.swift | 274 ++ .../DashSchemaV1+PersistentIndex.swift | 86 + .../DashSchemaV1+PersistentInvitation.swift | 73 + .../DashSchemaV1+PersistentKeyword.swift | 40 + .../DashSchemaV1+PersistentMasternode.swift | 185 + .../DashSchemaV1+PersistentPendingInput.swift | 42 + ...shSchemaV1+PersistentPlatformAddress.swift | 78 + ...PersistentPlatformAddressesSyncState.swift | 41 + .../DashSchemaV1+PersistentProperty.swift | 55 + .../DashSchemaV1+PersistentPublicKey.swift | 171 + ...hSchemaV1+PersistentShieldedActivity.swift | 89 + .../DashSchemaV1+PersistentShieldedNote.swift | 66 + ...emaV1+PersistentShieldedOutgoingNote.swift | 49 + ...SchemaV1+PersistentShieldedSyncState.swift | 34 + ...chemaV1+PersistentShieldedViewingKey.swift | 34 + .../DashSchemaV1+PersistentToken.swift | 376 ++ .../DashSchemaV1+PersistentTokenBalance.swift | 198 + ...SchemaV1+PersistentTokenHistoryEvent.swift | 112 + .../DashSchemaV1+PersistentTransaction.swift | 167 + .../DashSchemaV1+PersistentTxo.swift | 97 + .../DashSchemaV1+PersistentWallet.swift | 94 + ...maV1+PersistentWalletManagerMetadata.swift | 34 + .../DashSchemaV1+TokenTypes.swift | 153 + ...SchemaV2+PersistentTrackedMasternode.swift | 42 + .../DashSchemaV3+PersistentAssetLock.swift | 102 + .../Models/PersistentAssetLock.swift | 4 +- .../DashModelMigrationTests.swift | 18 +- .../swift-sdk/scripts/freeze_schema_models.py | 396 ++ 42 files changed, 4606 insertions(+), 3481 deletions(-) delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaFrozenModels.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAccount.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAssetLock.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentCoreAddress.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDPNSName.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactProfile.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactRequest.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayIgnoredSender.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayPayment.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayProfile.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDataContract.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocument.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocumentType.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIdentity.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIndex.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentInvitation.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentKeyword.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentMasternode.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPendingInput.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddress.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddressesSyncState.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentProperty.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPublicKey.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedActivity.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedNote.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedOutgoingNote.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedSyncState.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedViewingKey.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentToken.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenBalance.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenHistoryEvent.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTransaction.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTxo.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWallet.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWalletManagerMetadata.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+TokenTypes.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV2+PersistentTrackedMasternode.swift create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV3+PersistentAssetLock.swift create mode 100755 packages/swift-sdk/scripts/freeze_schema_models.py diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift index eaa0ff44317..8f9760f0e20 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift @@ -3,63 +3,27 @@ import SwiftData /// Factory for creating SwiftData model containers for Dash Platform persistence public enum DashModelContainer { - /// Every registered schema version's model list, parameterised on the - /// one model whose shape differs between versions. + /// The wallet-and-platform model graph as every released schema version + /// registered it, built from the frozen copies under `FrozenSchemas/` + /// and parameterised on the one slot whose frozen shape differs between + /// versions (`PersistentAssetLock`, which V3 changed). /// - /// Ordering is load-bearing only in the sense that it must not need to - /// change: keeping `assetLock` in the slot the live `PersistentAssetLock` - /// occupied means a frozen version's list is positionally identical to - /// what that version shipped. - private static func allModelTypes( - assetLock: any PersistentModel.Type - ) -> [any PersistentModel.Type] { - [ - PersistentIdentity.self, - PersistentDPNSName.self, - PersistentDashpayProfile.self, - PersistentDashpayContactProfile.self, - PersistentDashpayContactRequest.self, - PersistentDashpayPayment.self, - PersistentDashpayIgnoredSender.self, - PersistentDocument.self, - PersistentDataContract.self, - PersistentPublicKey.self, - PersistentTokenBalance.self, - PersistentKeyword.self, - PersistentToken.self, - PersistentDocumentType.self, - PersistentIndex.self, - PersistentProperty.self, - PersistentTokenHistoryEvent.self, - PersistentPlatformAddress.self, - PersistentPlatformAddressesSyncState.self, - PersistentWallet.self, - PersistentAccount.self, - PersistentCoreAddress.self, - PersistentTransaction.self, - PersistentTxo.self, - PersistentPendingInput.self, - PersistentWalletManagerMetadata.self, - PersistentShieldedNote.self, - PersistentShieldedOutgoingNote.self, - PersistentShieldedSyncState.self, - PersistentShieldedActivity.self, - PersistentShieldedViewingKey.self, - assetLock, - PersistentInvitation.self, - PersistentMasternode.self - ] - } - - - /// The V1/V2/V3 model set: frozen copies for every model in the - /// relationship component (see `DashSchemaFrozenModels.swift`), live - /// types for the eleven models outside it, and `assetLock` for the one - /// model whose shape differs between V2 and V3. + /// Every entry is a nested frozen type, never a live one. A released + /// version's checksum is the hash of every entity it declares — and a + /// relationship binds its destination by entity NAME, so a version that + /// mixed one live model into an otherwise frozen graph would have that + /// live model's current shape hashed into it (the entity name resolves + /// to whichever Swift type claimed it first in the process). Freezing + /// the whole relationship-connected graph per version is what keeps a + /// released checksum stable no matter what the live models do next, and + /// `DashModelMigrationTests` proves it against stores an older build + /// actually wrote. /// - /// Positionally identical to `allModelTypes` — a released version's - /// list must describe exactly the entities that version shipped. - private static func componentFrozenModelTypes( + /// Ordering is load-bearing only in the sense that it must not need to + /// change: keeping each model in the slot its live counterpart occupies + /// makes a frozen version's list positionally identical to what that + /// version shipped. + private static func frozenModelGraph( assetLock: any PersistentModel.Type ) -> [any PersistentModel.Type] { [ @@ -81,54 +45,90 @@ public enum DashModelContainer { DashSchemaV1.PersistentProperty.self, DashSchemaV1.PersistentTokenHistoryEvent.self, DashSchemaV1.PersistentPlatformAddress.self, - PersistentPlatformAddressesSyncState.self, + DashSchemaV1.PersistentPlatformAddressesSyncState.self, DashSchemaV1.PersistentWallet.self, DashSchemaV1.PersistentAccount.self, DashSchemaV1.PersistentCoreAddress.self, DashSchemaV1.PersistentTransaction.self, DashSchemaV1.PersistentTxo.self, DashSchemaV1.PersistentPendingInput.self, - PersistentWalletManagerMetadata.self, - PersistentShieldedNote.self, - PersistentShieldedOutgoingNote.self, - PersistentShieldedSyncState.self, - PersistentShieldedActivity.self, - PersistentShieldedViewingKey.self, + DashSchemaV1.PersistentWalletManagerMetadata.self, + DashSchemaV1.PersistentShieldedNote.self, + DashSchemaV1.PersistentShieldedOutgoingNote.self, + DashSchemaV1.PersistentShieldedSyncState.self, + DashSchemaV1.PersistentShieldedActivity.self, + DashSchemaV1.PersistentShieldedViewingKey.self, assetLock, - PersistentInvitation.self, - PersistentMasternode.self + DashSchemaV1.PersistentInvitation.self, + DashSchemaV1.PersistentMasternode.self ] } /// The exact model set registered as schema V1. Keep frozen: staged - /// migration identifies an existing store by this schema's checksum, so - /// this list may only reference models whose shape is frozen (see - /// `DashSchemaFrozenModels.swift`). + /// migration identifies an existing store by this schema's checksum. fileprivate static var v1ModelTypes: [any PersistentModel.Type] { - componentFrozenModelTypes(assetLock: DashSchemaV1.PersistentAssetLock.self) + frozenModelGraph(assetLock: DashSchemaV1.PersistentAssetLock.self) } /// The exact model set registered as schema V2 — V1 plus /// `PersistentTrackedMasternode`. Frozen for the same reason as /// `v1ModelTypes`. fileprivate static var v2ModelTypes: [any PersistentModel.Type] { - v1ModelTypes + [PersistentTrackedMasternode.self] + v1ModelTypes + [DashSchemaV2.PersistentTrackedMasternode.self] } - /// The exact model set registered as schema V3 — V2's frozen component - /// with the LIVE `PersistentAssetLock`, which is the only model V3 - /// changed. Frozen for the same reason as `v1ModelTypes`. + /// The exact model set registered as schema V3 — V2 with the asset-lock + /// shape that gained `recipientIsExternal`. Frozen for the same reason + /// as `v1ModelTypes`. fileprivate static var v3ModelTypes: [any PersistentModel.Type] { - componentFrozenModelTypes(assetLock: PersistentAssetLock.self) - + [PersistentTrackedMasternode.self] + frozenModelGraph(assetLock: DashSchemaV3.PersistentAssetLock.self) + + [DashSchemaV2.PersistentTrackedMasternode.self] } /// All persistent model types in the current Dash SDK schema (V4). - /// Unlike the lists above this one tracks the LIVE models, so it moves - /// whenever a model gains a property — which is exactly why the - /// released versions must not. + /// Unlike the released versions above this list tracks the LIVE models, + /// so it moves whenever a model gains a property — which is exactly why + /// the released versions must not. When the next property lands: freeze + /// every model here into the version being retired + /// (`scripts/freeze_schema_models.py`), add a version, add a stage. public static var modelTypes: [any PersistentModel.Type] { - allModelTypes(assetLock: PersistentAssetLock.self) + [PersistentTrackedMasternode.self] + [ + PersistentIdentity.self, + PersistentDPNSName.self, + PersistentDashpayProfile.self, + PersistentDashpayContactProfile.self, + PersistentDashpayContactRequest.self, + PersistentDashpayPayment.self, + PersistentDashpayIgnoredSender.self, + PersistentDocument.self, + PersistentDataContract.self, + PersistentPublicKey.self, + PersistentTokenBalance.self, + PersistentKeyword.self, + PersistentToken.self, + PersistentDocumentType.self, + PersistentIndex.self, + PersistentProperty.self, + PersistentTokenHistoryEvent.self, + PersistentPlatformAddress.self, + PersistentPlatformAddressesSyncState.self, + PersistentWallet.self, + PersistentAccount.self, + PersistentCoreAddress.self, + PersistentTransaction.self, + PersistentTxo.self, + PersistentPendingInput.self, + PersistentWalletManagerMetadata.self, + PersistentShieldedNote.self, + PersistentShieldedOutgoingNote.self, + PersistentShieldedSyncState.self, + PersistentShieldedActivity.self, + PersistentShieldedViewingKey.self, + PersistentAssetLock.self, + PersistentInvitation.self, + PersistentMasternode.self, + PersistentTrackedMasternode.self + ] } /// Create the schema for all Dash Platform models @@ -152,13 +152,37 @@ public enum DashModelContainer { groupContainer: groupContainer, cloudKitDatabase: cloudKit ? .automatic : .none ) + return try makeContainer(configuration: modelConfiguration) + } + + /// Open (or create) the store at an explicit file URL through the same + /// schema and migration plan as `create(cloudKit:groupContainer:)`. The + /// migration tests use it to open stores written by older builds exactly + /// the way the app would. + static func create(url: URL) throws -> ModelContainer { + let modelConfiguration = ModelConfiguration( + schema: schema, + url: url, + allowsSave: true, + cloudKitDatabase: .none + ) + return try makeContainer(configuration: modelConfiguration) + } + /// The one place a persistent container is built: the live schema is + /// constructed first (`schema`), and the container then runs the + /// migration plan over it. That order is what the frozen versions are + /// tested against, because it is the order under which a mixed + /// live/frozen graph would rebind a released version's entities. + private static func makeContainer( + configuration: ModelConfiguration + ) throws -> ModelContainer { // Always wire the migration plan so stores created by an older SDK // advance through the registered versioned schemas. - return try ModelContainer( + try ModelContainer( for: schema, migrationPlan: DashMigrationPlan.self, - configurations: [modelConfiguration] + configurations: [configuration] ) } @@ -169,12 +193,7 @@ public enum DashModelContainer { schema: schema, isStoredInMemoryOnly: true ) - - return try ModelContainer( - for: schema, - migrationPlan: DashMigrationPlan.self, - configurations: [modelConfiguration] - ) + return try makeContainer(configuration: modelConfiguration) } } @@ -383,8 +402,9 @@ public enum DashSchemaV3: VersionedSchema { /// Every column is additive with a default or optional and the index is /// additive, so a lightweight migration preserves each existing row. /// -/// Registering it required freezing the whole relationship component those -/// three models sit in — see `DashSchemaFrozenModels.swift`. +/// Registering it required freezing every model V1–V3 register — the +/// generated copies under `FrozenSchemas/`, see +/// `scripts/freeze_schema_models.py`. public enum DashSchemaV4: VersionedSchema { public static var versionIdentifier: Schema.Version { Schema.Version(4, 0, 0) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaFrozenModels.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaFrozenModels.swift deleted file mode 100644 index 1b535ec001d..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaFrozenModels.swift +++ /dev/null @@ -1,3384 +0,0 @@ -import Foundation -import SwiftData - -// MARK: - Frozen model definitions for already-released schema versions -// -// A `VersionedSchema` identifies a store by the CHECKSUM of the entities it -// declares, not by the identity of the Swift type list. So a registered -// schema version may only ever reference model types whose *shape* is frozen -// at the moment that version shipped. Pointing `DashSchemaVN.models` at a -// live `@Model` type means the next property added to that type silently -// mutates version N's checksum in place: a store written by the previously -// released binary then matches no schema in `DashMigrationPlan.schemas`, and -// `ModelContainer(for:migrationPlan:configurations:)` fails to open it with -// Cocoa error 134504 ("Cannot use staged migration with an unknown model -// version") instead of migrating it. -// -// This file holds the frozen copies. A frozen copy must be a *nested* type, -// because SwiftData derives the entity name from the unqualified type name — -// `DashSchemaV1.PersistentAssetLock` and the top-level `PersistentAssetLock` -// are two distinct Swift types that both describe the entity named -// "PersistentAssetLock", which is exactly what lets a migration stage map one -// onto the other. (`DashModelMigrationTests` asserts that entity naming, so a -// future SwiftData change to the derivation would fail loudly rather than -// silently renaming an entity.) -// -// ## Scope of the freeze -// -// Twenty-five of the 35 models are frozen here, in two groups: -// -// - `PersistentAssetLock`, frozen at its V2 shape (everything the live -// model has EXCEPT `recipientIsExternal`, which V3 added). Referenced by -// `DashSchemaV1.models` and `DashSchemaV2.models`; V3 and V4 reference -// the live type. -// - The 24 models of the relationship component that contains -// `PersistentTransaction`, `PersistentTxo`, `PersistentPendingInput` and -// `PersistentWallet`, frozen at their V3 shape (everything the live -// models had before V4's sweep columns). Referenced by V1, V2 and V3; -// V4 references the live types. The component travels as a whole -// because a frozen model must declare its relationships against frozen -// counterparts (an `inverse:` key path is typed on the destination -// model), and following those relationships in both directions closes -// over all 24 — while registering a frozen copy beside a live one for -// the SAME entity name is what a schema cannot express. -// -// The ten models outside the component (shielded storage, invitations, -// masternodes, the tracked-masternode registry, wallet-manager metadata, -// the platform-addresses sync state) are still referenced live by every -// version and still carry the latent defect described above. When the -// next change touches one of them, freeze it here too — and if it sits in -// a relationship component, freeze that component with it — then add a -// version and a stage. -// -// V1's own checksum has already drifted from what actually shipped as V1 -// (see the `DashSchemaV1` doc comment: several models were changed in place -// while V1 was the only registered version, and dev stores at V1 are -// knowingly expected to fail open and be rebuilt). The asset-lock copy -// below is therefore the shape as of the V2 release, shared by V1 and V2 — -// which is what makes V1 -> V2 continue to be "add -// `PersistentTrackedMasternode`" and nothing else, exactly as before. - -extension DashSchemaV1 { - /// `PersistentAssetLock` frozen at the shape it had when schema V2 - /// shipped — i.e. everything the live model has today EXCEPT - /// `recipientIsExternal`, which is what V3 adds. - /// - /// Referenced by both `DashSchemaV1.models` and `DashSchemaV2.models`. - /// Do not add properties here and do not "fix" its doc comments to - /// match the live model: every attribute, its optionality, its default - /// value, the `@Attribute(.unique)` marker and the `#Index` are all - /// inputs to the V2 checksum, and changing any of them re-breaks the - /// V2 stores this type exists to keep openable. Doc comments are not - /// inputs to the checksum, but keeping them minimal here keeps the - /// live model the single place worth reading. - /// - /// See the live ``SwiftDashSDK/PersistentAssetLock`` for what each - /// column means. - @Model - final class PersistentAssetLock { - #Index([\.walletId]) - - @Attribute(.unique) var outPointHex: String - var walletId: Data - var transactionBytes: Data - var fundingTypeRaw: Int - var identityIndexRaw: Int32 - var accountIndexRaw: Int32 = 0 - var amountDuffs: Int64 - var statusRaw: Int - var proofBytes: Data? - var recipientPlatformAddressHash: Data? - var recipientPlatformAddressType: UInt8? - var createdAt: Date - var updatedAt: Date - - init( - outPointHex: String, - walletId: Data, - transactionBytes: Data, - fundingTypeRaw: Int, - identityIndexRaw: Int32, - accountIndexRaw: Int32 = 0, - amountDuffs: Int64, - statusRaw: Int, - proofBytes: Data? = nil - ) { - self.outPointHex = outPointHex - self.walletId = walletId - self.transactionBytes = transactionBytes - self.fundingTypeRaw = fundingTypeRaw - self.identityIndexRaw = identityIndexRaw - self.accountIndexRaw = accountIndexRaw - self.amountDuffs = amountDuffs - self.statusRaw = statusRaw - self.proofBytes = proofBytes - self.createdAt = Date() - self.updatedAt = Date() - } - } -} - -// MARK: - The rest of the relationship component, frozen at the V3 shape -// -// The three models the sweep persistence changes — `PersistentTxo`, -// `PersistentPendingInput` and `PersistentWallet` — each gain a property -// (and `PersistentPendingInput` an index), so each needs a frozen copy for -// the same reason `PersistentAssetLock` did; `PersistentTransaction` is -// unchanged but sits in the same component. See the scope note at the top -// of this file for why the whole component travels together. -// -// These copies are the shape as of V3 — i.e. everything the live models had -// before the sweep columns — and are shared by V1, V2 and V3, none of which -// changed any model in this component. -// -// Do not edit these copies to match the live models. Every attribute, its -// optionality, its default, each `@Attribute` marker, each `#Index` and -// each relationship is an input to the V1/V2/V3 checksums, and changing one -// re-breaks the stores these types exist to keep openable. - -extension DashSchemaV1 { - @Model - final class PersistentAccount { - /// Compound uniqueness on the full account-identity tuple: - /// `(wallet, accountType, accountIndex, standardTag, - /// registrationIndex, keyClass, userIdentityId, - /// friendIdentityId)`. Mirrors the persister's match logic - /// exactly — the variant disambiguators (`standardTag` for - /// BIP44 vs BIP32, `registrationIndex` for top-ups, `keyClass` - /// for PlatformPayment) are part of the key so legitimate - /// sibling accounts can coexist (e.g. BIP44 #0 and BIP32 #0, - /// or multiple top-up accounts on the same identity). - #Unique([ - \.wallet, - \.accountType, - \.accountIndex, - \.standardTag, - \.registrationIndex, - \.keyClass, - \.userIdentityId, - \.friendIdentityId, - ]) - - /// Account type identifier — matches the `AccountTypeTagFFI` - /// discriminant from the Rust side (0 = Standard, 1 = CoinJoin, - /// … 14 = PlatformPayment, 15 = IdentityAuthenticationEcdsa, - /// 16 = IdentityAuthenticationBls). Stable across releases. - var accountType: UInt32 - /// Account index within the type (for indexed account types). For - /// `PlatformPayment` this is the `account` field; for - /// `DashpayReceivingFunds` / `DashpayExternalAccount` it's the - /// account-level selector; for - /// `IdentityAuthentication{Ecdsa,Bls}` it's the identity index. - var accountIndex: UInt32 - /// Human-readable account type name. - var accountTypeName: String - /// Per-account confirmed balance in duffs. - var balanceConfirmed: UInt64 - /// Per-account unconfirmed balance in duffs. - var balanceUnconfirmed: UInt64 - /// External address pool: highest used index (-1 = none). - var externalHighestUsed: Int32 - /// Internal (change) address pool: highest used index. - var internalHighestUsed: Int32 - /// `StandardAccountTypeTagFFI` value. Meaningful only when - /// `accountType == 0` (Standard): 0 = BIP44, 1 = BIP32. - var standardTag: UInt8 - /// `IdentityTopUp.registration_index`. Zero for other variants. - var registrationIndex: UInt32 - /// `PlatformPayment.key_class`. Zero for other variants. - var keyClass: UInt32 - /// `Dashpay*`.user_identity_id (32 bytes). Empty `Data` for other - /// variants. - var userIdentityId: Data - /// `Dashpay*`.friend_identity_id (32 bytes). Empty `Data` for - /// other variants. - var friendIdentityId: Data - /// Bincode-encoded extended public key for this account. For ECDSA - /// accounts it's an `ExtendedPubKey`; for the two provider - /// key-material accounts (`accountType == 10` operator = BLS, - /// `accountType == 11` platform node = Ed25519) it's the extended - /// BLS / Ed25519 public key instead. Populated by - /// `on_persist_account_registrations_fn`, consumed by - /// `on_load_wallet_list_fn` to reconstruct a watch-only account - /// (`Account::from_xpub` for ECDSA, `BLSAccount`/`EdDSAAccount` for - /// the provider accounts). `nil` means "not yet persisted" — - /// account cannot be restored silently. Unique because two - /// accounts can't legitimately share an xpub (would imply a key - /// reuse / derivation collision); SQL UNIQUE allows multiple - /// `nil` values, so freshly-inserted unhydrated rows don't - /// conflict. - @Attribute(.unique) var accountExtendedPubKeyBytes: Data? - /// Record timestamps. - var createdAt: Date - var lastUpdated: Date - - /// Parent wallet. Every account currently belongs to a wallet. If - /// standalone non-wallet accounts are introduced later, this - /// becomes optional again. - /// - /// Kept non-optional. SwiftData would otherwise fatal during - /// the `save()` phase of a wallet delete - /// (`Cannot remove PersistentWallet from relationship wallet on - /// PersistentAccount because an appropriate default value is - /// not configured`); the workaround is in - /// `PlatformWalletPersistenceHandler.deleteWalletData`, which - /// deletes all of the wallet's accounts in a separate - /// `save()` BEFORE deleting the wallet itself. By the time the - /// wallet row is deleted, its `accounts` collection is empty - /// and SwiftData has no inverse to null out. This costs - /// atomicity (two saves instead of one) — acceptable for a - /// user-initiated wipe. - var wallet: PersistentWallet - - /// Addresses from this account's address pools (external + - /// internal, or a single Absent pool for degenerate types). Holds - /// Core-chain (base58check) addresses only — PlatformPayment - /// accounts keep their addresses in `platformAddresses`. - /// Per-account TXOs flow through this collection - /// (`coreAddresses.flatMap(\.txos)`). - @Relationship(deleteRule: .cascade, inverse: \PersistentCoreAddress.account) - var coreAddresses: [PersistentCoreAddress] - - /// DIP-17 Platform Payment addresses for this account, keyed on - /// DIP-0018 bech32m encoding. Populated only when - /// `accountType == 14` (PlatformPayment). - @Relationship(deleteRule: .cascade, inverse: \PersistentPlatformAddress.account) - var platformAddresses: [PersistentPlatformAddress] - - /// Transactions this account participates in that the TXO graph - /// cannot recover — the payload-only involvement described in the - /// type doc above. Populated by the persistence handler, which - /// appends this account whenever it upserts a tx record the - /// changeset bucketed under this account, even when the record - /// produced no TXO here (special-tx payloads matching provider - /// owner / voting key addresses). - /// - /// A superset that overlaps the TXO-derived set for ordinary funded - /// txs (the handler appends there too), so consumers computing a - /// per-account transaction list must **union** this with the - /// TXO-derived txids and de-dup — see `AccountDetailView`. - /// - /// The `inverse:` for this many-to-many lives on - /// `PersistentTransaction.involvedAccounts`; this side carries the - /// plain declaration. Default `.nullify` delete rule — deleting - /// this account detaches it from each tx without removing the - /// (shared) tx rows. That matters for the wallet-wipe path - /// (`deleteWalletData`), which deletes accounts before the wallet: - /// `.nullify` on a to-many inverse has no "default value" fatal - /// (unlike the non-optional `wallet` back-reference), so no extra - /// pre-delete pass is needed. - var involvedTransactions: [PersistentTransaction] = [] - - init( - wallet: PersistentWallet, - accountType: UInt32, - accountIndex: UInt32, - accountTypeName: String - ) { - self.wallet = wallet - self.accountType = accountType - self.accountIndex = accountIndex - self.accountTypeName = accountTypeName - self.balanceConfirmed = 0 - self.balanceUnconfirmed = 0 - self.externalHighestUsed = -1 - self.internalHighestUsed = -1 - self.standardTag = 0 - self.registrationIndex = 0 - self.keyClass = 0 - self.userIdentityId = Data() - self.friendIdentityId = Data() - self.accountExtendedPubKeyBytes = nil - self.createdAt = Date() - self.lastUpdated = Date() - self.coreAddresses = [] - self.platformAddresses = [] - self.involvedTransactions = [] - } - } - - @Model - final class PersistentCoreAddress { - /// Base58check-encoded address. Unique across the SwiftData store - /// because the same address can't validly exist under two accounts - /// (collision would imply a wallet-id hash collision). - @Attribute(.unique) var address: String - /// Typed public key bytes, or empty Data when the Rust side couldn't - /// produce one (e.g. a pool entry that stored only a script). The - /// curve is given by `keyType`: 33-byte compressed secp256k1 (ECDSA), - /// 48-byte BLS operator key, or 32-byte Ed25519 platform-node key. - var publicKey: Data - /// `KeyTypeTagFFI` raw value identifying the curve of `publicKey`: - /// 0 ECDSA / 1 BLS / 2 EdDSA. Meaningful only when `publicKey` is - /// non-empty. The stored default (NOT just the init-parameter - /// default, which SwiftData migration never consults) keeps - /// pre-column stores openable: without it, lightweight migration - /// fails with "missing attribute values on mandatory destination - /// attribute" and the container refuses to load — a launch crash on - /// every device that has existing rows. Defaulted legacy rows read - /// as ECDSA with an empty `publicKey` until the next Rust - /// address-pool persist pulse (pool extension / address-used / - /// registration — NOT plain load, which only reads the snapshot) - /// re-emits them with typed keys. On load, Rust's - /// `restore_address_pool` keeps the pre-derived typed key when a - /// legacy row arrives key-less, so in-memory BLS operator matching - /// is unaffected; legacy Ed25519 platform-node keys are hardened-only - /// and re-derivable only via delete+re-import (pre-release - /// convention). - var keyType: UInt8 = 0 - /// `AddressPoolTypeTagFFI` raw value — 0 External, 1 Internal, - /// 2 Absent, 3 AbsentHardened. - var poolTypeTag: UInt8 - /// Derivation index within this pool. - var addressIndex: UInt32 - /// BIP32 derivation path (e.g. `"m/44'/1'/0'/0/3"`). - var derivationPath: String - /// Marked used by the Rust address pool (first-seen tx or explicit - /// `mark_used`). - var isUsed: Bool - /// SPV height where this address first appeared in a transaction. - /// Zero until the address is seen on-chain. - var firstSeenHeight: UInt32 - /// SPV height of the most recent transaction touching this address. - var lastSeenHeight: UInt32 - /// Cached balance in duffs from `AddressInfo.balance`. Updated by - /// subsequent `on_persist_account_address_pools_fn` pulses. - var balance: UInt64 - /// Record timestamps. - var createdAt: Date - var lastUpdated: Date - - /// Parent account. - var account: PersistentAccount? - - /// TXOs paid to this address. Cascade-delete: dropping the - /// address row takes its TXOs with it. The address is the - /// canonical owning record — no meaningful render path for an - /// address-less TXO. Pool rebuilds therefore need to reuse - /// existing rows (the persister upserts by Base58Check string, - /// which it already does) rather than wholesale-replace, or - /// the historical TXO chain gets wiped. - @Relationship(deleteRule: .cascade, inverse: \PersistentTxo.coreAddress) - var txos: [PersistentTxo] = [] - - init( - address: String, - publicKey: Data = Data(), - keyType: UInt8 = 0, - poolTypeTag: UInt8, - addressIndex: UInt32, - derivationPath: String, - isUsed: Bool = false, - balance: UInt64 = 0 - ) { - self.address = address - self.publicKey = publicKey - self.keyType = keyType - self.poolTypeTag = poolTypeTag - self.addressIndex = addressIndex - self.derivationPath = derivationPath - self.isUsed = isUsed - self.firstSeenHeight = 0 - self.lastSeenHeight = 0 - self.balance = balance - self.createdAt = Date() - self.lastUpdated = Date() - } - } - - @Model - final class PersistentDPNSName { - /// Compound uniqueness on `(networkRaw, normalizedParentDomainName, - /// normalizedLabel)`. Mirrors the DPNS contract's `domain` - /// document index `parentNameAndLabel` - /// (`normalizedParentDomainName + normalizedLabel`, `unique: true`) - /// and adds the network scope so two networks don't collide in a - /// shared local store. A label is only unique within a domain - /// on a given chain. - #Unique([\.networkRaw, \.normalizedParentDomainName, \.normalizedLabel]) - - /// Network discriminant. `UInt32` mirror of `Network.rawValue` — - /// Foundation's predicate engine compares it directly without a - /// custom converter. Stays in sync with `identity.networkRaw` - /// via the init; identities don't migrate between networks. - var networkRaw: UInt32 - - /// Type-safe accessor over `networkRaw`. Falls back to `.testnet` - /// if the stored raw value drifts — matches - /// `PersistentIdentity.network`. - var network: Network { - get { Network(rawValue: networkRaw) ?? .testnet } - set { networkRaw = newValue.rawValue } - } - - /// Display label — the original case-and-letters form the user - /// registered, e.g. "Alice". Maps to the DPNS document's - /// `label` property. - var label: String - - /// Homograph-safe lowercase form of `label` used for lookups - /// (e.g. "Alice" → "a11ce"; `o`/`O`→`0`, `i`/`I`→`1`, - /// `l`/`L`→`1`, everything else lowercased). Maps to the DPNS - /// document's `normalizedLabel` property and participates in the - /// per-domain uniqueness above. Computed once on insert from - /// `label` via `Self.normalize(_:)`. - var normalizedLabel: String - - /// Display parent domain — e.g. "dash". Maps to the DPNS - /// document's `parentDomainName` property. DPNS today only - /// supports the single top-level domain "dash", so the persister - /// stamps that as the default; the field exists so subdomain - /// support (when/if DPNS gains it) lands without a schema bump. - var parentDomainName: String - - /// Homograph-safe form of `parentDomainName` used for lookups. - /// Maps to the DPNS document's `normalizedParentDomainName` - /// property and participates in the per-domain uniqueness above. - var normalizedParentDomainName: String - - /// Unix-millis timestamp when the wallet first observed this - /// label belonging to the identity. Mirrors - /// `DpnsNameInfo.acquired_at`. `0` when unknown. - var acquiredAt: UInt64 - - /// Whether the latest canonical identity snapshot still includes this - /// name. Marketplace callbacks never overwrite this value. A name that - /// leaves the wallet keeps its row on the departed identity with `false`; - /// a same-wallet transfer rebinds the unique row to the current identity - /// with `true`. - var isOwned: Bool = true - - // MARK: - Username marketplace - // - // Fed by the `on_persist_dpns_name_states_fn` persister callback - // (`DpnsNameStateFFI`), NOT by the identity label snapshot that - // populates the fields above. All of them are optional or defaulted - // so an existing store migrates in place (SwiftData lightweight - // migration). - // - // READ CONTRACT: every field in this section is meaningful only - // while `documentIdBase58` is non-nil. A nil document id means the - // wallet is not tracking this name's marketplace state — it does NOT - // mean the name is owned and unlisted. Gate any marketplace UI on - // `documentIdBase58 != nil` before reading `saleStatus` or - // `priceCredits`. - - /// Base58 id of the DPNS `domain` document behind this label — the - /// handle every trade transition needs, stable across transfers and - /// purchases. `nil` while no marketplace state has been mirrored (or - /// after the row was dropped from marketplace tracking). - var documentIdBase58: String? - - /// Listed sale price in **credits** (1 duff = 1000 credits), stored - /// as `Int64(bitPattern:)` like `PersistentIdentity.balance` because - /// SwiftData has no unsigned 64-bit column. `nil` = the name is not - /// listed for sale, which is distinct from a 0-credit listing. - var priceCredits: Int64? - - /// Raw ``DpnsNameSaleStatus`` discriminant: 0 = owned, 1 = sold, - /// 2 = transferred. Defaults to 0 so existing rows migrate, so read - /// it through ``saleStatus`` rather than directly. - var saleStatusRaw: Int16 = 0 - - /// Base58 id of the counterparty a departed name went to — the buyer - /// when `saleStatusRaw == 1`, the recipient when it is 2. `nil` while - /// the name is still owned (or the counterparty is unknown). - var counterpartyIdBase58: String? - - /// Domain document `$createdAt` in Unix milliseconds. `nil` when - /// Platform did not carry the timestamp. - var documentCreatedAtMs: UInt64? - - /// Domain document `$updatedAt` in Unix milliseconds. `nil` when - /// Platform did not carry the timestamp. - var documentUpdatedAtMs: UInt64? - - /// Domain document `$transferredAt` in Unix milliseconds. `nil` when - /// Platform did not carry the timestamp. - var documentTransferredAtMs: UInt64? - - /// Unix-millis timestamp of the sync pass / confirmed transition - /// that last wrote the marketplace fields. `0` = never written. - var marketplaceUpdatedAt: UInt64 = 0 - - // MARK: - Relationships - - /// Owning identity. Cascade-deleted from the parent — losing the - /// identity row should drop its label cache too. The `inverse` - /// declaration on `PersistentIdentity.dpnsNames` is the source of - /// truth for this association. - /// - /// Non-optional: every DPNS-label row exists *because* of an - /// identity. The persister wires it at construction time - /// (before insert) so SwiftData's non-optional relationship - /// contract is honored. - var identity: PersistentIdentity - - // MARK: - Timestamps - - var createdAt: Date - var lastUpdated: Date - - // MARK: - Initialization - - init( - identity: PersistentIdentity, - label: String, - parentDomainName: String = "dash", - acquiredAt: UInt64 = 0, - isOwned: Bool = true - ) { - self.identity = identity - self.networkRaw = identity.networkRaw - self.label = label - self.normalizedLabel = label.lowercased() - self.parentDomainName = parentDomainName - self.normalizedParentDomainName = parentDomainName.lowercased() - self.acquiredAt = acquiredAt - self.isOwned = isOwned - // A freshly inserted row carries no marketplace state until the - // marketplace persister callback writes it — hence a nil document - // id, which is the "not tracked" signal the read contract above - // documents. - self.documentIdBase58 = nil - self.priceCredits = nil - self.saleStatusRaw = 0 - self.counterpartyIdBase58 = nil - self.documentCreatedAtMs = nil - self.documentUpdatedAtMs = nil - self.documentTransferredAtMs = nil - self.marketplaceUpdatedAt = 0 - self.createdAt = Date() - self.lastUpdated = Date() - } - } - - @Model - final class PersistentDashpayContactProfile { - /// Compound uniqueness on `(networkRaw, ownerIdentityId, - /// contactIdentityId)`. Mirrors the per-owner, per-contact keying of - /// the Rust `contact_profiles` map. - #Unique([ - \.networkRaw, \.ownerIdentityId, \.contactIdentityId - ]) - - /// Network discriminant. `UInt32` mirror of `Network.rawValue` — - /// Foundation's predicate engine compares it directly without a - /// custom converter. Kept in sync with `owner.networkRaw` by the - /// init. - var networkRaw: UInt32 - - /// Type-safe accessor over `networkRaw`. Falls back to `.testnet` - /// if the stored raw value drifts. - var network: Network { - get { Network(rawValue: networkRaw) ?? .testnet } - set { networkRaw = newValue.rawValue } - } - - /// Owning (wallet-managed) identity's 32-byte id, denormalized so - /// `#Predicate` filters can match without a relationship traversal - /// through the `owner` join. Always equal to `owner.identityId` — - /// kept in sync by the persister. - var ownerIdentityId: Data - - /// The contact's 32-byte identity id — the `contact_profiles` map - /// key. Part of the compound unique key above. - var contactIdentityId: Data - - // MARK: - Profile fields - // - // All optional — every `dashpay.profile` document field is optional - // in the contract schema except the implicit `$ownerId`. We mirror - // that so partial profiles (only an `avatarUrl` set, only a - // `displayName` set, etc.) round-trip without forcing placeholders. - - /// `displayName` field on the contact's DashPay `profile` document. - var displayName: String? - - /// `publicMessage` field on the contact's `profile` document. - var publicMessage: String? - - /// `bio` field. Carried for forwards-compat with future contract - /// revisions; reserved here so adding it later doesn't trigger a - /// destructive schema change. - var bio: String? - - /// `avatarUrl` field — URL the consumer fetches + caches locally. - /// The binary asset itself is never persisted. Treated as untrusted - /// (attacker-controlled public data): the Rust side caches and - /// restores it only when it is a bounded `https://` URL. - var avatarUrl: String? - - /// `avatarHash` field — 32-byte hash of the avatar binary, so - /// consumers can verify a fetched asset matches what the contact - /// published. `nil` when the underlying `avatar_hash` was absent. - var avatarHash: Data? - - /// `avatarFingerprint` field — 8-byte perceptual hash for quick - /// equality checks on cached avatars. `nil` when absent. - var avatarFingerprint: Data? - - /// Wall-clock ms of the last fetch attempt on the Rust side - /// (`ContactProfileEntry.checked_at_ms`) — drives the self-heal - /// backoff. Round-tripped verbatim so the restored cache keeps the - /// same re-query schedule it had before relaunch. Stored as the - /// scalar so the predicate engine compares it directly. - var checkedAtMs: UInt64 - - // MARK: - Relationships - - /// Owning identity — the wallet-managed identity whose cached - /// contact profiles this row belongs to. Non-optional: every contact - /// profile exists *because of* an owner identity. Cascade-deleted - /// from `PersistentIdentity.contactProfiles`. - var owner: PersistentIdentity - - // MARK: - Timestamps (local row bookkeeping) - - var createdAt: Date - var lastUpdated: Date - - // MARK: - Initialization - - init( - owner: PersistentIdentity, - contactIdentityId: Data, - checkedAtMs: UInt64, - displayName: String? = nil, - publicMessage: String? = nil, - bio: String? = nil, - avatarUrl: String? = nil, - avatarHash: Data? = nil, - avatarFingerprint: Data? = nil - ) { - self.owner = owner - self.networkRaw = owner.networkRaw - self.ownerIdentityId = owner.identityId - self.contactIdentityId = contactIdentityId - self.checkedAtMs = checkedAtMs - self.displayName = displayName - self.publicMessage = publicMessage - self.bio = bio - self.avatarUrl = avatarUrl - self.avatarHash = avatarHash - self.avatarFingerprint = avatarFingerprint - self.createdAt = Date() - self.lastUpdated = Date() - } - } - - @Model - final class PersistentDashpayContactRequest { - /// Compound uniqueness on `(networkRaw, ownerIdentityId, - /// contactIdentityId, isOutgoing)`. Mirrors the per-direction - /// keying the Rust changeset uses on - /// `ContactChangeSet::sent_requests` / - /// `incoming_requests`, scoped by network so two networks don't - /// collide in a shared local store. - #Unique([ - \.networkRaw, \.ownerIdentityId, \.contactIdentityId, \.isOutgoing - ]) - - /// Network discriminant. `UInt32` mirror of `Network.rawValue` — - /// Foundation's predicate engine compares it directly without a - /// custom converter. Kept in sync with `owner.networkRaw` by the - /// init. - var networkRaw: UInt32 - - /// Type-safe accessor over `networkRaw`. Falls back to `.testnet` - /// if the stored raw value drifts. - var network: Network { - get { Network(rawValue: networkRaw) ?? .testnet } - set { networkRaw = newValue.rawValue } - } - - /// Owning (wallet-managed) identity's 32-byte id, denormalized so - /// `#Predicate` filters can match without a relationship traversal - /// through the optional `owner` join. Always equal to - /// `owner.identityId` — kept in sync by the persister. - var ownerIdentityId: Data - - /// Other party's 32-byte identity id. For outgoing rows this is - /// the recipient (`ContactRequest::recipient_id`); for incoming - /// rows this is the sender (`ContactRequest::sender_id`). The - /// `isOutgoing` bit disambiguates which direction this row - /// represents. - var contactIdentityId: Data - - /// Direction bit. `true` ⇒ owner sent this request to contact; - /// `false` ⇒ contact sent this request to owner. Same shape as - /// the Rust `ContactRequestFFI::is_outgoing` field. - var isOutgoing: Bool - - // MARK: - Payload — round-trips `ContactRequest` verbatim - - /// `ContactRequest::sender_key_index` — index of the sender's - /// identity public key used for the ECDH that encrypted the - /// payload. - var senderKeyIndex: UInt32 - - /// `ContactRequest::recipient_key_index`. - var recipientKeyIndex: UInt32 - - /// `ContactRequest::account_reference` — DashPay account derivation - /// hint the sender encoded in the request. - var accountReference: UInt32 - - /// `ContactRequest::encrypted_public_key` bytes. Always non-empty - /// — every contact-request document carries an encrypted key. - var encryptedPublicKey: Data - - /// `ContactRequest::encrypted_account_label` bytes, when present. - /// `nil` mirrors the source `Option` being `None`. - var encryptedAccountLabel: Data? - - /// `ContactRequest::auto_accept_proof` bytes, when present. `nil` - /// mirrors the source `Option` being `None`. - var autoAcceptProof: Data? - - /// `ContactRequest::core_height_created_at` — the Core block - /// height at which the request landed on Platform. - var coreHeightCreatedAt: UInt32 - - /// `ContactRequest::created_at` — Unix-millis timestamp the - /// request document was created. - var createdAtMillis: UInt64 - - /// Whether the established relationship this row belongs to has a - /// **permanently broken** payment channel. Mirrors - /// `ContactRequestFFI::payment_channel_broken`: only meaningful - /// for rows projected from the `established` map — both - /// directions of an established pair carry the same flag (it's a - /// property of the relationship, not of one direction). Always - /// `false` for pending rows. The UI reads it to disable "Send - /// Dash" and surface "payment channel broken — ask the contact to - /// send a new request". - /// - /// Defaulted so existing rows ride SwiftData's lightweight - /// migration (additive column, non-destructive). - var paymentChannelBroken: Bool = false - - /// Owner-private alias for the contact — `contactInfo`-backed, - /// synced across devices via Platform. Mirrors - /// `ContactRequestFFI::alias`; established rows only, replicated - /// onto both directions like `paymentChannelBroken`. Optional so - /// existing rows ride the lightweight migration. - var contactAlias: String? - - /// Owner-private note — same conventions as `contactAlias`. - var contactNote: String? - - /// `contactInfo.displayHidden` — whether the owner hid this - /// contact from the list. Defaulted for lightweight migration. - var contactHidden: Bool = false - - /// The contact's decrypted DIP-15 `encryptedAccountLabel` — the label - /// the contact chose for the account they shared (a payment-routing - /// hint, e.g. "Main wallet"). **System-derived and read-only**, unlike - /// the owner-private `contactAlias`/`contactNote`: it is decrypted in - /// Rust from the contact's incoming request, so it is populated only on - /// the incoming-direction row (the outgoing row carries a label *we* - /// sent, which is not surfaced). Optional so existing rows ride the - /// lightweight migration. - var contactAccountLabel: String? - - /// `EstablishedContact::accepted_accounts` — the DIP-15 - /// rotated-account acceptances for this relationship. Mirrors - /// `ContactRequestFFI::accepted_accounts`: a property of the - /// relationship (not one direction), so it is replicated onto - /// both directions like `paymentChannelBroken`; always empty for - /// pending rows. Defaulted to an empty array so existing rows - /// ride SwiftData's lightweight migration. - var contactAcceptedAccounts: [UInt32] = [] - - // MARK: - Relationships - - /// Owning identity — the wallet-managed identity this row's - /// `ownerIdentityId` denormalizes. Non-optional: every - /// contact-request row exists *because of* an owner identity. - /// Cascade-deleted from `PersistentIdentity.contactRequests`. - var owner: PersistentIdentity - - // MARK: - Timestamps - - var createdAt: Date - var lastUpdated: Date - - // MARK: - Initialization - - init( - owner: PersistentIdentity, - contactIdentityId: Data, - isOutgoing: Bool, - senderKeyIndex: UInt32, - recipientKeyIndex: UInt32, - accountReference: UInt32, - encryptedPublicKey: Data, - encryptedAccountLabel: Data? = nil, - autoAcceptProof: Data? = nil, - coreHeightCreatedAt: UInt32, - createdAtMillis: UInt64, - paymentChannelBroken: Bool = false - ) { - self.owner = owner - self.networkRaw = owner.networkRaw - self.ownerIdentityId = owner.identityId - self.contactIdentityId = contactIdentityId - self.isOutgoing = isOutgoing - self.senderKeyIndex = senderKeyIndex - self.recipientKeyIndex = recipientKeyIndex - self.accountReference = accountReference - self.encryptedPublicKey = encryptedPublicKey - self.encryptedAccountLabel = encryptedAccountLabel - self.autoAcceptProof = autoAcceptProof - self.coreHeightCreatedAt = coreHeightCreatedAt - self.createdAtMillis = createdAtMillis - self.paymentChannelBroken = paymentChannelBroken - self.createdAt = Date() - self.lastUpdated = Date() - } - } - - @Model - final class PersistentDashpayIgnoredSender { - /// Compound uniqueness on `(networkRaw, ownerIdentityId, - /// ignoredSenderId)` — the Rust per-sender suppression key, scoped by - /// network so two networks don't collide in a shared store. - #Unique([ - \.networkRaw, \.ownerIdentityId, \.ignoredSenderId - ]) - - /// Network discriminant. `UInt32` mirror of `Network.rawValue`, kept - /// in sync with `owner.networkRaw` by the init. - var networkRaw: UInt32 - - /// Type-safe accessor over `networkRaw`. Falls back to `.testnet` if - /// the stored raw value drifts. - var network: Network { - get { Network(rawValue: networkRaw) ?? .testnet } - set { networkRaw = newValue.rawValue } - } - - /// Owning (wallet-managed) identity's 32-byte id — the recipient that - /// ignored the sender. Denormalized so `#Predicate` filters match - /// without a relationship traversal. Always equal to - /// `owner.identityId`. - var ownerIdentityId: Data - - /// The 32-byte id of the ignored sender. The per-sender suppression - /// key — no `accountReference`, so ALL of this sender's requests are - /// suppressed. - var ignoredSenderId: Data - - // MARK: - Relationships - - /// Owning identity — the wallet-managed identity that ignored the - /// sender. Non-optional: an ignore exists *because of* an owner - /// identity. Cascade-deleted from - /// `PersistentIdentity.dashpayIgnoredSenders`. - var owner: PersistentIdentity - - // MARK: - Timestamps (local row bookkeeping) - - var ignoredAt: Date - - // MARK: - Initialization - - init( - owner: PersistentIdentity, - ignoredSenderId: Data - ) { - self.owner = owner - self.networkRaw = owner.networkRaw - self.ownerIdentityId = owner.identityId - self.ignoredSenderId = ignoredSenderId - self.ignoredAt = Date() - } - } - - @Model - final class PersistentDashpayPayment { - /// Compound uniqueness on `(networkRaw, ownerIdentityId, txid)`. - /// Mirrors the per-identity txid keying of the Rust - /// `dashpay_payments` map. - #Unique([ - \.networkRaw, \.ownerIdentityId, \.txid - ]) - - /// Network discriminant. `UInt32` mirror of `Network.rawValue` — - /// Foundation's predicate engine compares it directly without a - /// custom converter. Kept in sync with `owner.networkRaw` by the - /// init. - var networkRaw: UInt32 - - /// Type-safe accessor over `networkRaw`. Falls back to `.testnet` - /// if the stored raw value drifts. - var network: Network { - get { Network(rawValue: networkRaw) ?? .testnet } - set { networkRaw = newValue.rawValue } - } - - /// Owning (wallet-managed) identity's 32-byte id, denormalized so - /// `#Predicate` filters can match without a relationship traversal - /// through the `owner` join. Always equal to `owner.identityId` — - /// kept in sync by the refresh path. - var ownerIdentityId: Data - - /// The other identity in this payment - /// (`DashpayPaymentFFI::counterparty_id`). Whether they are the - /// sender or the receiver is encoded in `directionRaw`. - var counterpartyIdentityId: Data - - /// Amount in duffs. Always positive; `directionRaw` carries the - /// sign. - var amountDuffs: UInt64 - - /// Raw `DashPayPaymentDirection` value. Stored as the scalar so - /// the predicate engine compares it directly. - var directionRaw: UInt8 - - /// Type-safe accessor over `directionRaw`. Falls back to `.sent` - /// if the stored raw value drifts. - var direction: DashPayPaymentDirection { - get { DashPayPaymentDirection(rawValue: directionRaw) ?? .sent } - set { directionRaw = newValue.rawValue } - } - - /// Raw `DashPayPaymentStatus` value. - var statusRaw: UInt8 - - /// Type-safe accessor over `statusRaw`. Falls back to `.pending` - /// if the stored raw value drifts. - var status: DashPayPaymentStatus { - get { DashPayPaymentStatus(rawValue: statusRaw) ?? .pending } - set { statusRaw = newValue.rawValue } - } - - /// Transaction id (hex), the Rust `dashpay_payments` map key. - /// Part of the compound unique key above. - var txid: String - - /// Sender memo, when present. `nil` mirrors the source `Option` - /// being `None`. - var memo: String? - - // MARK: - Relationships - - /// Owning identity — the wallet-managed identity whose payment - /// history this row belongs to. Non-optional: every payment row - /// exists *because of* an owner identity. Cascade-deleted from - /// `PersistentIdentity.dashpayPayments`. - var owner: PersistentIdentity - - // MARK: - Timestamps (local row bookkeeping, not payment dates) - - var createdAt: Date - var lastUpdated: Date - - // MARK: - Initialization - - init( - owner: PersistentIdentity, - counterpartyIdentityId: Data, - amountDuffs: UInt64, - direction: DashPayPaymentDirection, - status: DashPayPaymentStatus, - txid: String, - memo: String? = nil - ) { - self.owner = owner - self.networkRaw = owner.networkRaw - self.ownerIdentityId = owner.identityId - self.counterpartyIdentityId = counterpartyIdentityId - self.amountDuffs = amountDuffs - self.directionRaw = direction.rawValue - self.statusRaw = status.rawValue - self.txid = txid - self.memo = memo - self.createdAt = Date() - self.lastUpdated = Date() - } - } - - @Model - final class PersistentDashpayProfile { - /// Compound uniqueness on `(networkRaw, identity)`. Mirrors the - /// DashPay contract's per-`ownerId` uniqueness on the `profile` - /// document, scoped by network so two networks don't collide in a - /// shared local store. - #Unique([\.networkRaw, \.identity]) - - /// Network discriminant. `UInt32` mirror of `Network.rawValue` — - /// Foundation's predicate engine compares it directly without a - /// custom converter. Stays in sync with `identity.networkRaw` - /// (set by the init); identities don't migrate between networks. - var networkRaw: UInt32 - - /// Type-safe accessor over `networkRaw`. Falls back to `.testnet` - /// if the stored raw value drifts — matches - /// `PersistentIdentity.network`. - var network: Network { - get { Network(rawValue: networkRaw) ?? .testnet } - set { networkRaw = newValue.rawValue } - } - - // MARK: - Profile fields - // - // All optional — every `dashpay.profile` document field is - // optional in the contract schema except the implicit - // `$ownerId`. We mirror that on the row so partial profiles - // (only an `avatarUrl` set, only a `displayName` set, etc.) - // round-trip without forcing placeholder values. - - /// `displayName` field on the DashPay `profile` document. Up to - /// 25 chars per the contract schema. - var displayName: String? - - /// `publicMessage` field on the DashPay `profile` document. Up to - /// 140 chars per the contract schema. - var publicMessage: String? - - /// `bio` field. Not part of the v3 DashPay contract today; the - /// FFI carries the slot for forwards-compat with future contract - /// revisions and the column is reserved here so adding it doesn't - /// trigger a destructive schema change. - var bio: String? - - /// `avatarUrl` field. URL string the consumer is expected to - /// fetch + cache locally; the binary asset itself is never - /// persisted on this row. - var avatarUrl: String? - - /// `avatarHash` field — 32-byte hash of the avatar binary, - /// stored alongside the URL so consumers can verify the fetched - /// asset matches what the profile author published. `nil` when - /// the underlying `avatar_hash` was `None`. - var avatarHash: Data? - - /// `avatarFingerprint` field — 8-byte perceptual hash for - /// quick equality checks on cached avatars without rehashing the - /// full asset. `nil` when the underlying `avatar_fingerprint` - /// was `None`. - var avatarFingerprint: Data? - - // MARK: - Relationships - - /// Owning identity. Non-optional — a profile only exists in the - /// context of an identity. Cascade-deleted from the parent's - /// `dashpayProfile` relationship; the persister wires this up at - /// construction time. - var identity: PersistentIdentity - - // MARK: - Timestamps - - var createdAt: Date - var lastUpdated: Date - - // MARK: - Initialization - - init( - identity: PersistentIdentity, - displayName: String? = nil, - publicMessage: String? = nil, - bio: String? = nil, - avatarUrl: String? = nil, - avatarHash: Data? = nil, - avatarFingerprint: Data? = nil - ) { - self.identity = identity - self.networkRaw = identity.networkRaw - self.displayName = displayName - self.publicMessage = publicMessage - self.bio = bio - self.avatarUrl = avatarUrl - self.avatarHash = avatarHash - self.avatarFingerprint = avatarFingerprint - self.createdAt = Date() - self.lastUpdated = Date() - } - } - - @Model - final class PersistentDataContract { - /// Index `networkRaw` so the static `predicate(networkRaw:)` and - /// `tokensPredicate(networkRaw:)` helpers — plus every per-network - /// list view — can index-scan instead of table-scan. - #Index([\.networkRaw]) - - @Attribute(.unique) var id: Data - var name: String - var serializedContract: Data - var createdAt: Date - var lastAccessedAt: Date - - // Binary serialization (CBOR format) - var binarySerialization: Data? - - // Version info - var version: Int? - var ownerId: Data? - - // Keywords and description - @Relationship(deleteRule: .cascade, inverse: \PersistentKeyword.dataContract) - var keywordRelations: [PersistentKeyword] - var contractDescription: String? - - // Schema and document types storage - var schemaData: Data - var documentTypesData: Data - - // Groups - var groupsData: Data? - - // Network - /// Stored as the `Network.rawValue` `UInt32` so SwiftData - /// `#Predicate` expressions can evaluate it directly. See - /// `PersistentIdentity.networkRaw` for the full rationale. - var networkRaw: UInt32 - - /// Type-safe accessor over `networkRaw`. Setter writes through. - var network: Network { - get { Network(rawValue: networkRaw) ?? .testnet } - set { networkRaw = newValue.rawValue } - } - - // Timestamps - var lastUpdated: Date - var lastSyncedAt: Date? - - // Contract configuration - var canBeDeleted: Bool - var readonly: Bool - var keepsHistory: Bool - var schemaDefs: Int? - - // Document defaults - var documentsKeepHistoryContractDefault: Bool - var documentsMutableContractDefault: Bool - var documentsCanBeDeletedContractDefault: Bool - - // Relationships with cascade delete - @Relationship(deleteRule: .cascade, inverse: \PersistentToken.dataContract) - var tokens: [PersistentToken]? - - @Relationship(deleteRule: .cascade, inverse: \PersistentDocumentType.dataContract) - var documentTypes: [PersistentDocumentType]? - - @Relationship(deleteRule: .cascade, inverse: \PersistentDocument.dataContract) - var documents: [PersistentDocument] - - // Owner identity — populated when the owner happens to also live in - // the local store. May be nil even when `ownerId` is set, because - // most contracts in the local cache will be owned by identities the - // user doesn't hold. Back-filled lazily by - // `ContractIdentityLinker.linkContractToOwner` when either side is - // inserted. - @Relationship(deleteRule: .nullify, inverse: \PersistentIdentity.ownedDataContracts) - var ownerIdentity: PersistentIdentity? - - // Token support tracking - var hasTokens: Bool - var tokensData: Data? - - // Computed properties - var idBase58: String { - id.toBase58String() - } - - var ownerIdBase58: String? { - ownerId?.toBase58String() - } - - var parsedContract: [String: Any]? { - try? JSONSerialization.jsonObject(with: serializedContract, options: []) as? [String: Any] - } - - var binarySerializationHex: String? { - binarySerialization?.toHexString() - } - - var keywords: [String] { - keywordRelations.map { $0.keyword } - } - - var schema: [String: Any] { - get { - guard let json = try? JSONSerialization.jsonObject(with: schemaData), - let dict = json as? [String: Any] else { - return [:] - } - return dict - } - set { - schemaData = (try? JSONSerialization.data(withJSONObject: newValue)) ?? Data() - lastUpdated = Date() - } - } - - var documentTypesList: [String] { - get { - guard let json = try? JSONSerialization.jsonObject(with: documentTypesData), - let array = json as? [String] else { - return [] - } - return array - } - set { - documentTypesData = (try? JSONSerialization.data(withJSONObject: newValue)) ?? Data() - lastUpdated = Date() - } - } - - var tokenConfigurations: [String: Any]? { - get { - guard let data = tokensData, - let json = try? JSONSerialization.jsonObject(with: data), - let dict = json as? [String: Any] else { - return nil - } - return dict - } - set { - if let newValue = newValue { - tokensData = try? JSONSerialization.data(withJSONObject: newValue) - hasTokens = true - } else { - tokensData = nil - hasTokens = false - } - lastUpdated = Date() - } - } - - var groups: [String: Any]? { - get { - guard let data = groupsData, - let json = try? JSONSerialization.jsonObject(with: data), - let dict = json as? [String: Any] else { - return nil - } - return dict - } - set { - if let newValue = newValue { - groupsData = try? JSONSerialization.data(withJSONObject: newValue) - } else { - groupsData = nil - } - lastUpdated = Date() - } - } - - init( - id: Data, - name: String, - serializedContract: Data, - version: Int? = 1, - ownerId: Data? = nil, - schema: [String: Any] = [:], - documentTypesList: [String] = [], - keywords: [String] = [], - description: String? = nil, - hasTokens: Bool = false, - network: Network - ) { - self.id = id - self.name = name - self.serializedContract = serializedContract - self.createdAt = Date() - self.lastAccessedAt = Date() - self.version = version - self.ownerId = ownerId - - // Schema and document types - self.schemaData = (try? JSONSerialization.data(withJSONObject: schema)) ?? Data() - self.documentTypesData = (try? JSONSerialization.data(withJSONObject: documentTypesList)) ?? Data() - - // Keywords - self.keywordRelations = keywords.map { PersistentKeyword(keyword: $0, contractId: id.toBase58String()) } - self.contractDescription = description - - // Tokens - self.hasTokens = hasTokens - self.tokensData = nil - - // Groups - self.groupsData = nil - - // Documents - self.documents = [] - - // Owner identity link is back-filled later by - // `ContractIdentityLinker`. Initialise explicitly because - // SwiftData's auto-init of optional relationships has - // historically been flaky enough in this codebase to be - // worth the line. - self.ownerIdentity = nil - - // Network and timestamps - self.networkRaw = network.rawValue - self.lastUpdated = Date() - self.lastSyncedAt = nil - - // Default values for contract configuration - self.canBeDeleted = false - self.readonly = false - self.keepsHistory = false - self.documentsKeepHistoryContractDefault = false - self.documentsMutableContractDefault = true - self.documentsCanBeDeletedContractDefault = true - } - - func updateLastAccessed() { - self.lastAccessedAt = Date() - } - - func updateVersion(_ newVersion: Int) { - self.version = newVersion - self.lastUpdated = Date() - } - - func markAsSynced() { - self.lastSyncedAt = Date() - } - - func addDocument(_ document: PersistentDocument) { - documents.append(document) - lastUpdated = Date() - } - - func removeDocument(withId documentId: String) { - if let docIdData = Data.identifier(fromBase58: documentId) { - documents.removeAll { $0.id == docIdData } - } - lastUpdated = Date() - } - } - - @Model - final class PersistentDocument { - /// Index `networkRaw` to keep per-network document scans - /// index-served. The static `predicate(contractId:network:)` helper - /// and every UI list view filter by the active network. - #Index([\.networkRaw]) - - // Primary key - @Attribute(.unique) var documentId: String - - // Core document properties - var documentType: String - var revision: Int32 - var data: Data - - // References (stored as strings for queries) - var contractId: String - var ownerId: String - - // Binary data for efficient operations - var contractIdData: Data - var ownerIdData: Data - - // Timestamps - var createdAt: Date - var updatedAt: Date - var transferredAt: Date? - - // Block heights - var createdAtBlockHeight: Int64? - var updatedAtBlockHeight: Int64? - var transferredAtBlockHeight: Int64? - - // Core block heights - var createdAtCoreBlockHeight: Int64? - var updatedAtCoreBlockHeight: Int64? - var transferredAtCoreBlockHeight: Int64? - - // Network - /// Stored as the `Network.rawValue` `UInt32` so SwiftData - /// `#Predicate` expressions can evaluate it directly. See - /// `PersistentIdentity.networkRaw` for the full rationale. - var networkRaw: UInt32 - - /// Type-safe accessor over `networkRaw`. Setter writes through. - var network: Network { - get { Network(rawValue: networkRaw) ?? .testnet } - set { networkRaw = newValue.rawValue } - } - - // Deletion flag - var isDeleted: Bool = false - - // Local tracking - var localCreatedAt: Date - var localUpdatedAt: Date - - // Relationships - var documentType_relation: PersistentDocumentType? - var dataContract: PersistentDataContract? - - // Optional reference to local identity (if owner is local) - var ownerIdentity: PersistentIdentity? - - // Computed properties - var id: Data { - Data.identifier(fromBase58: documentId) ?? Data() - } - - var idBase58: String { - documentId - } - - var ownerIdBase58: String { - ownerId - } - - var contractIdBase58: String { - contractId - } - - var properties: [String: Any]? { - try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] - } - - var displayTitle: String { - guard let props = properties else { return "Document" } - - if let title = props["title"] as? String { return title } - if let name = props["name"] as? String { return name } - if let label = props["label"] as? String { return label } - if let normalizedLabel = props["normalizedLabel"] as? String { return normalizedLabel } - - return documentType - } - - var summary: String { - var parts: [String] = [] - - parts.append("Type: \(documentType)") - parts.append("Rev: \(revision)") - - // Pin to Gregorian so the `createdAt` year stays CE even - // when the device is configured for a non-Gregorian - // calendar (e.g. Thai region → Buddhist era). The SDK - // doesn't depend on the app's `AppDate` helper, so we - // configure the formatter inline. - let formatter = DateFormatter() - formatter.calendar = Calendar(identifier: .gregorian) - formatter.dateStyle = .short - parts.append("Created: \(formatter.string(from: createdAt))") - - return parts.joined(separator: " • ") - } - - init( - documentId: String, - documentType: String, - revision: Int32, - data: Data, - contractId: String, - ownerId: String, - network: Network - ) { - self.documentId = documentId - self.documentType = documentType - self.revision = revision - self.data = data - self.contractId = contractId - self.ownerId = ownerId - self.contractIdData = Data.identifier(fromBase58: contractId) ?? Data() - self.ownerIdData = Data.identifier(fromBase58: ownerId) ?? Data() - self.networkRaw = network.rawValue - self.createdAt = Date() - self.updatedAt = Date() - self.localCreatedAt = Date() - self.localUpdatedAt = Date() - } - - // MARK: - Methods - func updateProperties(_ newData: Data) { - self.data = newData - self.updatedAt = Date() - } - - func updateRevision(_ newRevision: Int64) { - self.revision = Int32(newRevision) - self.updatedAt = Date() - } - - func markAsDeleted() { - self.isDeleted = true - self.updatedAt = Date() - } - - // MARK: - Static Methods - static func predicate(documentId: String) -> Predicate { - #Predicate { doc in - doc.documentId == documentId && doc.isDeleted == false - } - } - - static func predicate(contractId: String, network: Network) -> Predicate { - // See `PersistentIdentity.predicate(network:)` — Foundation's - // predicate engine can't capture `Network`, so we filter on - // the UInt32-backed `networkRaw` shadow field. - let target = network.rawValue - return #Predicate { doc in - doc.contractId == contractId && doc.networkRaw == target && doc.isDeleted == false - } - } - - static func predicate(ownerId: Data) -> Predicate { - let ownerIdString = ownerId.toBase58String() - return #Predicate { doc in - doc.ownerId == ownerIdString && doc.isDeleted == false - } - } - - // MARK: - Identity Linking - func linkToLocalIdentityIfNeeded(in modelContext: ModelContext) { - guard ownerIdentity == nil else { return } - - let ownerIdToMatch = self.ownerIdData - let identityPredicate = #Predicate { identity in - identity.identityId == ownerIdToMatch && identity.isLocal == true - } - - let descriptor = FetchDescriptor(predicate: identityPredicate) - - do { - if let localIdentity = try modelContext.fetch(descriptor).first { - self.ownerIdentity = localIdentity - self.localUpdatedAt = Date() - } - } catch { - print("Failed to link document to local identity: \(error)") - } - } - } - - @Model - final class PersistentDocumentType { - @Attribute(.unique) var id: Data - var contractId: Data - var name: String - - // Schema stored as JSON - var schemaJSON: Data - var propertiesJSON: Data - - // Document behavior settings - var documentsKeepHistory: Bool - var documentsMutable: Bool - var documentsCanBeDeleted: Bool - var documentsTransferable: Bool - - // indexOnly storage mode (meta-schema v3, protocol version 14): no - // stored rows — the index entries ARE the documents - var indexOnly: Bool = false - - // Required fields - var requiredFieldsJSON: Data? - - // Security - var securityLevel: Int - - // Trade and creation restrictions - var tradeMode: Int - var creationRestrictionMode: Int - - // Identity encryption keys - var requiresIdentityEncryptionBoundedKey: Bool - var requiresIdentityDecryptionBoundedKey: Bool - - // Timestamps - var createdAt: Date - var lastAccessedAt: Date - - // Relationship to data contract - var dataContract: PersistentDataContract? - - // Relationship to documents - @Relationship(deleteRule: .cascade, inverse: \PersistentDocument.documentType_relation) - var documents: [PersistentDocument]? - - // Relationship to indices - @Relationship(deleteRule: .cascade, inverse: \PersistentIndex.documentType) - var indices: [PersistentIndex]? - - // Relationship to properties - @Relationship(deleteRule: .cascade, inverse: \PersistentProperty.documentType) - var propertiesList: [PersistentProperty]? - - init(contractId: Data, name: String, schemaJSON: Data, propertiesJSON: Data) { - // Create unique ID by combining contract ID and name - var idData = contractId - idData.append(name.data(using: .utf8) ?? Data()) - self.id = idData - - self.contractId = contractId - self.name = name - self.schemaJSON = schemaJSON - self.propertiesJSON = propertiesJSON - self.documentsKeepHistory = false - self.documentsMutable = true - self.documentsCanBeDeleted = true - self.documentsTransferable = false - self.securityLevel = 0 - self.tradeMode = 0 - self.creationRestrictionMode = 0 - self.requiresIdentityEncryptionBoundedKey = false - self.requiresIdentityDecryptionBoundedKey = false - self.createdAt = Date() - self.lastAccessedAt = Date() - } - } - - @Model - final class PersistentIdentity { - /// Index `networkRaw` so per-network scans (`#Predicate { $0.networkRaw == raw }`) - /// don't degrade to a table scan. Every UI surface that lists - /// identities filters by the active network. - #Index([\.networkRaw]) - - // MARK: - Core Properties - @Attribute(.unique) var identityId: Data - var balance: Int64 - var revision: Int64 - /// `true` iff this identity is YOURS or deliberately tracked on - /// this device, two ways in: - /// - wallet-derived: identities of a wallet on this device are - /// ALWAYS local — the persister promotes the flag when it - /// attaches the `wallet` relationship, and the startup heal - /// repairs rows persisted before that rule existed; - /// - manually added: the user loaded/watched the identity via a - /// UI flow (LoadIdentityView by id/name), which marks its own - /// row (the initializer default `true` matches — a directly - /// constructed row is a manual add). - /// - /// `false` only for incidental rows — observed foreign - /// identities materialized by sync that nobody asked to track. - /// The flag is PROMOTE-ONLY: no sync path ever writes `false` - /// over a `true` (a manual mark must survive Platform data - /// flowing over the row, and losing a wallet link doesn't - /// un-track an identity). - /// - /// It makes no claim about signing capability — compute that - /// live where needed; wallet-owned filtering has - /// `walletOwnedIdentitiesPredicate`. - var isLocal: Bool - var alias: String? - /// User's chosen primary display label (the one rendered on - /// list rows and avatars). Populated only when the user selects a - /// main name from `mainDpnsName` selection or as the fallback set - /// during initial registration. The full label collection lives on - /// the `dpnsNames` relationship below; this scalar is just the - /// "show this one in the cell" hint. - var dpnsName: String? - var mainDpnsName: String? - var identityType: String - - // MARK: - Special Key Storage (stored in keychain) - var votingPrivateKeyIdentifier: String? - var ownerPrivateKeyIdentifier: String? - var payoutPrivateKeyIdentifier: String? - - // MARK: - Public Keys - @Relationship(deleteRule: .cascade) var publicKeys: [PersistentPublicKey] - - // MARK: - Timestamps - var createdAt: Date - var lastUpdated: Date - var lastSyncedAt: Date? - - // MARK: - Network - /// Stored as the `Network.rawValue` `UInt32` so SwiftData - /// `#Predicate` expressions can evaluate it directly. Foundation's - /// predicate engine rejects captured non-primitive types — even - /// Codable raw-value enums crash at evaluation with - /// "Unsupported Predicate: Captured/constant values of type - /// 'Network' are not supported". The `network` computed - /// accessor below keeps the public API type-safe; only predicates - /// that need to filter by network reach for `networkRaw`. - var networkRaw: UInt32 - - /// Type-safe accessor over `networkRaw`. Reads fall back to - /// `.testnet` if the stored raw value ever drifts out of the - /// `Network` range (shouldn't happen — writers only go through - /// this setter which uses `Network.rawValue`). - var network: Network { - get { Network(rawValue: networkRaw) ?? .testnet } - set { networkRaw = newValue.rawValue } - } - - // MARK: - Wallet Association - // - // Cardinality: an identity belongs to 0 or 1 wallet. A wallet - // holds N identities (see `PersistentWallet.identities`). When - // the wallet is deleted, `wallet` nulls out (deleteRule: - // `.nullify`) and the identity row survives orphaned. - // - // The `wallet` reference is the single source of truth — there - // is no denormalized scalar `walletId`. Callers that want the - // 32-byte wallet id read `identity.wallet?.walletId`; - // predicates filter with `$0.wallet?.walletId == target`. - // `@Relationship` is declared on the `PersistentWallet` side - // (`identities`, with `inverse: \PersistentIdentity.wallet`), - // so this is a plain stored property. - var wallet: PersistentWallet? - /// DIP-9 identity index within the owning wallet. Mirrors the - /// `identity_index` carried on `IdentityEntryFFI` from Rust. - /// Only meaningful when `wallet != nil`; defaults to 0 - /// otherwise. Used to stable-sort identities within a wallet - /// (e.g. when grouping public keys by identity). - var identityIndex: UInt32 = 0 - - // MARK: - Relationships - @Relationship(deleteRule: .cascade, inverse: \PersistentDocument.ownerIdentity) var documents: [PersistentDocument] - @Relationship(deleteRule: .nullify) var tokenBalances: [PersistentTokenBalance] - - /// Confirmed DPNS labels observed for this identity. Cascade-deleted from - /// the parent — losing the identity row drops the label cache and retained - /// marketplace history too. A name that leaves this wallet remains related - /// to its departed identity for history with - /// `PersistentDPNSName.isOwned == false`. A transfer to another identity in - /// the same wallet instead rebinds the schema's single unique-name row to - /// the current owner. Owned-name surfaces use - /// `PersistentDPNSName.predicate(identityId:)`. - @Relationship(deleteRule: .cascade, inverse: \PersistentDPNSName.identity) - var dpnsNames: [PersistentDPNSName] = [] - - /// DashPay profile cache for this identity — at most one row per - /// (network, identity) per the contract's per-`ownerId` - /// uniqueness on the `profile` document. Cascade-deleted from the - /// parent. Optional because not every identity has published a - /// profile (and the FFI changeset's `dashpay_profile: None` - /// semantics mean "no update", not "delete" — the persister never - /// nils this out from a flush). Inserted / refreshed by - /// `PlatformWalletPersistenceHandler.upsertDashpayProfile(...)`. - @Relationship(deleteRule: .cascade, inverse: \PersistentDashpayProfile.identity) - var dashpayProfile: PersistentDashpayProfile? - - /// DashPay contact-request rows owned by this identity (both - /// outgoing and incoming). Cascade-deleted from the parent. Same - /// query-by-denormalized-id pattern as `dpnsNames`: filters use - /// `PersistentDashpayContactRequest.predicate(ownerIdentityId:)` - /// rather than walking this collection from a SwiftUI view. - /// Append / overwrite / delete on the write path: the persister - /// callback applies upserts (per `(owner, contact, isOutgoing)`) - /// and tombstones (`removed_sent` / `removed_incoming`) directly. - @Relationship(deleteRule: .cascade, inverse: \PersistentDashpayContactRequest.owner) - var contactRequests: [PersistentDashpayContactRequest] = [] - - /// DashPay payment-history rows owned by this identity. - /// Cascade-deleted from the parent. Same - /// query-by-denormalized-id pattern as `contactRequests`: filters - /// use `PersistentDashpayPayment.predicate(ownerIdentityId:)` - /// rather than walking this collection from a SwiftUI view. - /// Populated by `PlatformWalletManager.refreshDashPayPayments` - /// (FFI getter → upsert), not by the persister callback. - @Relationship(deleteRule: .cascade, inverse: \PersistentDashpayPayment.owner) - var dashpayPayments: [PersistentDashpayPayment] = [] - - /// DashPay ignored senders (per-sender mute, = block, reversible, - /// local-only) owned by this identity. Cascade-deleted from the parent. - /// Persisted from the `ignored` changeset array by `persistContacts` - /// and read back at load to rebuild the Rust `ignored_senders` set — - /// without them an ignored sender resurfaces on relaunch. Filters use - /// `PersistentDashpayIgnoredSender.predicate(ownerIdentityId:)`. - @Relationship(deleteRule: .cascade, inverse: \PersistentDashpayIgnoredSender.owner) - var dashpayIgnoredSenders: [PersistentDashpayIgnoredSender] = [] - - /// Cached DashPay **contact** profiles owned by this identity (one - /// per contact whose public profile has been fetched). Cascade-deleted - /// from the parent. Same query-by-denormalized-id pattern as - /// `contactRequests`: filters use - /// `PersistentDashpayContactProfile.predicate(ownerIdentityId:)` rather - /// than walking this collection from a SwiftUI view. Populated by the - /// persister callback (`IdentityEntryFFI.contact_profiles` rows) and - /// read back at load to rebuild the Rust `contact_profiles` map. - /// Distinct from the owner's own `dashpayProfile`. - @Relationship(deleteRule: .cascade, inverse: \PersistentDashpayContactProfile.owner) - var contactProfiles: [PersistentDashpayContactProfile] = [] - - // Contracts in the local store that name this identity as their - // owner. `.nullify` so deleting the identity leaves the contract - // rows alive (with `ownerIdentity` nulled) — matches the user's - // intent that contracts persist independently of whether the owner - // identity happens to be loaded. - // The `@Relationship` macro is declared on the contract side - // (`PersistentDataContract.ownerIdentity`) so this is a plain - // stored property — see `wallet` above for the same pattern. - var ownedDataContracts: [PersistentDataContract] - - // MARK: - Initialization - init( - identityId: Data, - balance: Int64 = 0, - revision: Int64 = 0, - isLocal: Bool = true, - alias: String? = nil, - dpnsName: String? = nil, - mainDpnsName: String? = nil, - identityType: IdentityType = .user, - votingPrivateKeyIdentifier: String? = nil, - ownerPrivateKeyIdentifier: String? = nil, - payoutPrivateKeyIdentifier: String? = nil, - network: Network, - identityIndex: UInt32 = 0 - ) { - self.identityId = identityId - self.balance = balance - self.revision = revision - self.isLocal = isLocal - self.alias = alias - self.dpnsName = dpnsName - self.mainDpnsName = mainDpnsName - self.identityType = identityType.rawValue - self.votingPrivateKeyIdentifier = votingPrivateKeyIdentifier - self.ownerPrivateKeyIdentifier = ownerPrivateKeyIdentifier - self.payoutPrivateKeyIdentifier = payoutPrivateKeyIdentifier - self.networkRaw = network.rawValue - self.identityIndex = identityIndex - self.publicKeys = [] - self.documents = [] - self.tokenBalances = [] - self.dpnsNames = [] - self.dashpayProfile = nil - self.contactRequests = [] - self.dashpayPayments = [] - self.dashpayIgnoredSenders = [] - self.contactProfiles = [] - self.ownedDataContracts = [] - self.createdAt = Date() - self.lastUpdated = Date() - self.lastSyncedAt = nil - } - - // MARK: - Computed Properties - var identityIdString: String { - identityId.toHexString() - } - - var identityIdBase58: String { - identityId.toBase58String() - } - - var formattedBalance: String { - let dashAmount = Double(balance) / 100_000_000_000 - return String(format: "%.8f DASH", dashAmount) - } - - /// User-facing short name. Priority: `alias` → `mainDpnsName` - /// → `dpnsName` → truncated hex id. Mirrors the old - /// `IdentityModel.displayName` extension so views that read - /// this don't change behavior post-migration. - var displayName: String { - if let alias = alias, !alias.isEmpty { - return alias - } - if let mainDpnsName = mainDpnsName, !mainDpnsName.isEmpty { - return mainDpnsName - } - if let dpnsName = dpnsName, !dpnsName.isEmpty { - return dpnsName - } - return String(identityIdString.prefix(12)) + "..." - } - - var identityTypeEnum: IdentityType { - IdentityType(rawValue: identityType) ?? .user - } - - // MARK: - Methods - func updateBalance(_ newBalance: Int64) { - self.balance = newBalance - self.lastUpdated = Date() - } - - func updateRevision(_ newRevision: Int64) { - self.revision = newRevision - self.lastUpdated = Date() - } - - func markAsSynced() { - self.lastSyncedAt = Date() - } - - func updateDPNSName(_ name: String?) { - self.dpnsName = name - self.lastUpdated = Date() - } - - func addPublicKey(_ key: PersistentPublicKey) { - publicKeys.append(key) - lastUpdated = Date() - } - - func removePublicKey(withId keyId: Int32) { - publicKeys.removeAll { $0.keyId == keyId } - lastUpdated = Date() - } - } - - @Model - final class PersistentIndex { - @Attribute(.unique) var id: Data - var contractId: Data - var documentTypeName: String - var name: String - - // Index configuration - var unique: Bool - var nullSearchable: Bool - var contested: Bool - - // Count / sum axes (meta-schema v3, protocol version 14). Every - // keyword is persisted VERBATIM as authored in the contract JSON — - // `countable` keeps its boolean-or-string spelling ("true" / - // "countable" / "countableAllowingOffset"), and the `averageable` / - // `rangeAverageable` sugar is stored as-is rather than desugared. - // Interpreting the spellings (DPP's normalization rules) is protocol - // logic and stays out of the SDK; display layers map them for - // presentation. - var countable: String? - var rangeCountable: Bool = false - var summable: String? - var rangeSummable: Bool = false - var averageable: String? - var rangeAverageable: Bool = false - - // Ranking axes (each adds one ordered secondary tree) - var rankedCountable: Bool = false - var rankedSummable: Bool = false - var rankedAverageable: Bool = false - - // indexOnly member key (the property whose value keys each entry). - // Persisted only when declared; an omitted terminal on an indexOnly - // type means $ownerId per DPP, a default display layers apply. - var terminal: String? - - // Preallocation: creating the refersTo-referenced document also - // creates this index's trees, and deleting the last entry keeps them - var preallocated: Bool = false - - // Time-range bucketing transform ({on, range, step, phase}), if any - var timeRangeJSON: Data? - - // Properties in the index with sorting - var propertiesJSON: Data - - // Contested details (if contested) - var contestedDetailsJSON: Data? - - // Timestamps - var createdAt: Date - - // Relationship to document type - var documentType: PersistentDocumentType? - - init(contractId: Data, documentTypeName: String, name: String, properties: [String]) { - // Create unique ID by combining contract ID, document type name, and index name - var idData = contractId - idData.append(documentTypeName.data(using: .utf8) ?? Data()) - idData.append(name.data(using: .utf8) ?? Data()) - self.id = idData - - self.contractId = contractId - self.documentTypeName = documentTypeName - self.name = name - self.unique = false - self.nullSearchable = false - self.contested = false - - // Store properties as JSON array - if let jsonData = try? JSONSerialization.data(withJSONObject: properties, options: []) { - self.propertiesJSON = jsonData - } else { - self.propertiesJSON = Data() - } - - self.createdAt = Date() - } - } - - @Model - final class PersistentKeyword { - @Attribute(.unique) var id: String - var keyword: String - var contractId: String - - // Relationship - var dataContract: PersistentDataContract? - - init(keyword: String, contractId: String) { - self.id = "\(contractId)_\(keyword)" - self.keyword = keyword - self.contractId = contractId - } - } - - @Model - final class PersistentPendingInput { - /// Two single-column indexes: - /// * `outpoint` — the per-outpoint reconciliation lookup that - /// runs on every `upsertUtxo`. - /// * `walletId` — per-wallet pending-input scans (cleanup when - /// a wallet is removed, the storage explorer's network - /// scope, "long-lived non-zero pending count" diagnostics). - /// - /// SwiftData allows only a single `#Index` macro per model; - /// passing multiple key-path arrays declares multiple separate - /// indexes from one macro call. - #Index([\.outpoint], [\.walletId]) - var outpoint: Data - - /// Position of this input in the spending transaction's input - /// list. Carried so a future UI surface can render the input - /// index correctly without re-deriving from the raw tx bytes; - /// the resolution flow itself only uses `outpoint`. - var inputIndex: UInt32 - - /// 32-byte canonical txid of the spending transaction. Stored - /// in addition to the relationship below so the entry remains - /// usable if the parent `PersistentTransaction` isn't yet in the - /// background context (re-upsert ordering, fault-in lag, …). - var spendingTxid: Data - - /// The transaction this input belongs to. Cascade-deleted from - /// the parent side via `PersistentTransaction.pendingInputs` so - /// removing a tx doesn't leave dangling pending rows. - var spendingTransaction: PersistentTransaction? - - /// Wallet id (`PersistentTxo.walletId` denorm) so cleanup / - /// per-wallet diagnostics can scope without joining through the - /// transaction relationship. - var walletId: Data - - /// Insertion timestamp — useful for spotting stale entries that - /// never resolved (orphans whose previous output isn't ours). - var createdAt: Date - - init( - outpoint: Data, - inputIndex: UInt32, - spendingTxid: Data, - spendingTransaction: PersistentTransaction?, - walletId: Data - ) { - self.outpoint = outpoint - self.inputIndex = inputIndex - self.spendingTxid = spendingTxid - self.spendingTransaction = spendingTransaction - self.walletId = walletId - self.createdAt = Date() - } - } - - @Model - final class PersistentPlatformAddress { - /// Index `walletId` so per-wallet platform-address scans — - /// `predicate(walletId:)`, the storage explorer's network scope - /// fallback, BLAST-sync re-upsert paths — hit an index instead - /// of scanning the whole table. - #Index([\.walletId]) - - /// DIP-0018 bech32m-encoded address (`dash1…` / `tdash1…`). Unique - /// across the SwiftData store — a collision would imply a wallet- - /// id / derivation path collision. - @Attribute(.unique) var address: String - /// `PlatformAddress` type byte: 0 = P2PKH, 1 = P2SH. Matches the - /// discriminant emitted by the Rust-side FFI. - var addressType: UInt8 - /// 20-byte address hash. Kept denormalized so the BLAST balance - /// callback (which gets hashes, not full addresses) can upsert in - /// one fetch. - @Attribute(.unique) var addressHash: Data - /// 33-byte compressed secp256k1 public key, or empty Data if the - /// Rust side couldn't produce one (pool entries that stored only - /// a script, etc.). - var publicKey: Data - /// DIP-17 account index (field `account` in `PlatformPayment`). - var accountIndex: UInt32 - /// DIP-17 derivation index within the account. - var addressIndex: UInt32 - /// BIP32 derivation path (e.g. `"m/9'/5'/17'/0'/0'/0"`). - var derivationPath: String - /// Marked used by the Rust address pool (first-seen tx or explicit - /// `mark_used`), or auto-flipped by BLAST when a non-zero - /// balance / nonce first arrives. - var isUsed: Bool - /// Credit balance in credits (1e11 credits per DASH). - var balance: UInt64 - /// Current anti-replay nonce. - var nonce: UInt32 - /// Platform block height where this address first appeared in a - /// balance changeset. Zero until the address is seen on-chain. - var firstSeenHeight: UInt32 - /// Platform block height this row's `balance` is current **as of** - /// — the balance height pin (`AddressFunds::as_of_height` in Rust). - /// Round-tripped verbatim through the persistence callbacks so the - /// sync's delta-replay gate survives restarts. Zero means "unknown - /// provenance" (rows persisted before the pin existed). - var lastSeenHeight: UInt64 - /// 32-byte wallet ID that owns this address. Denormalized from - /// `account.wallet.walletId` so per-wallet `@Query` filters don't - /// have to traverse two optional relationships. - var walletId: Data - /// Record timestamps. - var createdAt: Date - var lastUpdated: Date - - /// Parent account (PlatformPayment, type tag 14). - var account: PersistentAccount? - - init( - address: String, - addressType: UInt8, - addressHash: Data, - publicKey: Data = Data(), - accountIndex: UInt32, - addressIndex: UInt32, - derivationPath: String, - isUsed: Bool = false, - balance: UInt64 = 0, - nonce: UInt32 = 0, - walletId: Data - ) { - self.address = address - self.addressType = addressType - self.addressHash = addressHash - self.publicKey = publicKey - self.accountIndex = accountIndex - self.addressIndex = addressIndex - self.derivationPath = derivationPath - self.isUsed = isUsed - self.balance = balance - self.nonce = nonce - self.firstSeenHeight = 0 - self.lastSeenHeight = 0 - self.walletId = walletId - self.createdAt = Date() - self.lastUpdated = Date() - } - } - - @Model - final class PersistentProperty { - @Attribute(.unique) var id: Data - var contractId: Data - var documentTypeName: String - var name: String - - // Property type and constraints - var type: String - var format: String? - var contentMediaType: String? - var byteArray: Bool - var minItems: Int? - var maxItems: Int? - var pattern: String? - var minLength: Int? - var maxLength: Int? - var minValue: Int? - var maxValue: Int? - var fieldDescription: String? - - // Property attributes - var transient: Bool - var isRequired: Bool - - // Timestamps - var createdAt: Date - - // Relationship to document type - var documentType: PersistentDocumentType? - - init(contractId: Data, documentTypeName: String, name: String, type: String) { - // Create unique ID by combining contract ID, document type name, and property name - var idData = contractId - idData.append(documentTypeName.data(using: .utf8) ?? Data()) - idData.append(name.data(using: .utf8) ?? Data()) - self.id = idData - - self.contractId = contractId - self.documentTypeName = documentTypeName - self.name = name - self.type = type - self.byteArray = false - self.transient = false - self.isRequired = false - self.createdAt = Date() - } - } - - @Model - final class PersistentPublicKey { - // MARK: - Core Properties - var keyId: Int32 - var purpose: String - var securityLevel: String - var keyType: String - var readOnly: Bool - var disabledAt: Int64? - - // MARK: - Key Data - var publicKeyData: Data - - // MARK: - Contract Bounds - /// JSON-encoded `[base64(contractId)]` — legacy storage shape - /// that only retains the contract id, never the document-type - /// name. New code paths still write here for the id portion; - /// `contractBoundsDocumentTypeName` carries the doc-type so - /// the `SingleContractDocumentType` variant round-trips - /// faithfully. Keeping the field shape lets old SwiftData - /// stores that predate the doc-type column continue to load - /// without migration (the doc-type column is just `nil`). - var contractBoundsData: Data? - - /// When set, the key's bounds are - /// `.singleContractDocumentType(id: contractBoundsData[0], - /// documentTypeName: contractBoundsDocumentTypeName)`. When - /// `nil`, the key is either unbounded (when `contractBoundsData` - /// is also nil) or bounded to a whole contract via - /// `.singleContract(id:)`. Optional so old stores load cleanly. - var contractBoundsDocumentTypeName: String? - - // MARK: - Private Key Reference (optional) - var privateKeyKeychainIdentifier: String? - - // MARK: - Derivation breadcrumb (derive-sign-destroy) - /// 32-byte wallet id that owns this identity key, denormalized from the - /// discovery breadcrumb. Paired with `identityDerivationPath`, it lets the - /// signer derive this key on demand from the Keychain-held seed instead of - /// reading a stored scalar. `nil` for rows persisted before this column - /// existed and for keys with no wallet association; such rows fall back to - /// the stored scalar until the backfill populates them. Additive optional - /// column => SwiftData lightweight migration. - var walletId: Data? - - /// Full DIP-9 identity-authentication path - /// `m/9'/coin'/5'/0'/ECDSA'/identityIndex'/keyIndex'` the signer feeds to - /// the mnemonic resolver to derive this key's private scalar at sign time. - /// The authoritative breadcrumb; `nil` until written on persist or - /// backfilled from the key's Keychain metadata. - var identityDerivationPath: String? - - // MARK: - Metadata - var identityId: String - var createdAt: Date - var lastAccessed: Date? - - // MARK: - Relationships - @Relationship(inverse: \PersistentIdentity.publicKeys) - var identity: PersistentIdentity? - - // MARK: - Initialization - init( - keyId: Int32, - purpose: KeyPurpose, - securityLevel: SecurityLevel, - keyType: KeyType, - publicKeyData: Data, - readOnly: Bool = false, - disabledAt: Int64? = nil, - contractBounds: [Data]? = nil, - contractBoundsDocumentTypeName: String? = nil, - identityId: String - ) { - self.keyId = keyId - self.purpose = String(purpose.rawValue) - self.securityLevel = String(securityLevel.rawValue) - self.keyType = String(keyType.rawValue) - self.publicKeyData = publicKeyData - self.readOnly = readOnly - self.disabledAt = disabledAt - if let contractBounds = contractBounds { - self.contractBoundsData = try? JSONSerialization.data(withJSONObject: contractBounds.map { $0.base64EncodedString() }) - } else { - self.contractBoundsData = nil - } - self.contractBoundsDocumentTypeName = contractBoundsDocumentTypeName - self.identityId = identityId - self.createdAt = Date() - } - - // MARK: - Computed Properties - var contractBounds: [Data]? { - get { - guard let data = contractBoundsData, - let json = try? JSONSerialization.jsonObject(with: data), - let strings = json as? [String] else { - return nil - } - return strings.compactMap { Data(base64Encoded: $0) } - } - set { - // Always clear the doc-type column when the contract- - // bounds ids change through this setter. The - // `documentTypeName` is paired with a SPECIFIC id, so - // mutating ids without explicitly carrying the doc- - // type would leave the columns inconsistent and make - // `toIdentityPublicKey()` reconstruct a stale variant. - // Callers that want the full `.singleContractDocumentType` - // round-trip should write `contractBoundsDocumentTypeName` - // explicitly after this setter, or go through - // `PersistentPublicKey.from(IdentityPublicKey, identityId:)` - // which sets both columns atomically. - contractBoundsDocumentTypeName = nil - if let newValue = newValue { - contractBoundsData = try? JSONSerialization.data(withJSONObject: newValue.map { $0.base64EncodedString() }) - } else { - contractBoundsData = nil - } - } - } - - var purposeEnum: KeyPurpose? { - guard let purposeInt = UInt8(purpose) else { return nil } - return KeyPurpose(rawValue: purposeInt) - } - - var securityLevelEnum: SecurityLevel? { - guard let levelInt = UInt8(securityLevel) else { return nil } - return SecurityLevel(rawValue: levelInt) - } - - var keyTypeEnum: KeyType? { - guard let typeInt = UInt8(keyType) else { return nil } - return KeyType(rawValue: typeInt) - } - - var isDisabled: Bool { - disabledAt != nil - } - - /// Check if this public key has an associated private key identifier - var hasPrivateKeyIdentifier: Bool { - privateKeyKeychainIdentifier != nil - } - } - - @Model - final class PersistentToken { - @Attribute(.unique) var id: Data - var contractId: Data - var position: Int - var name: String - - // Basic token supply info - var baseSupply: String - var maxSupply: String? - var decimals: Int - - // Token conventions - var localizations: [String: TokenLocalization]? - - // Status flags - var isPaused: Bool - var allowTransferToFrozenBalance: Bool - - // History keeping rules - var keepsTransferHistory: Bool - var keepsFreezingHistory: Bool - var keepsMintingHistory: Bool - var keepsBurningHistory: Bool - var keepsDirectPricingHistory: Bool - var keepsDirectPurchaseHistory: Bool - - // Control rules - var conventionsChangeRules: ChangeControlRules? - var maxSupplyChangeRules: ChangeControlRules? - var manualMintingRules: ChangeControlRules? - var manualBurningRules: ChangeControlRules? - var freezeRules: ChangeControlRules? - var unfreezeRules: ChangeControlRules? - var destroyFrozenFundsRules: ChangeControlRules? - var emergencyActionRules: ChangeControlRules? - - // Distribution rules - var perpetualDistribution: TokenPerpetualDistribution? - var preProgrammedDistribution: TokenPreProgrammedDistribution? - var newTokensDestinationIdentity: Data? - var mintingAllowChoosingDestination: Bool - var distributionChangeRules: TokenDistributionChangeRules? - - // Marketplace rules - var tradeMode: TokenTradeMode - var tradeModeChangeRules: ChangeControlRules? - - // Main control group - var mainControlGroupPosition: Int? - var mainControlGroupCanBeModified: String? - - // Description - var tokenDescription: String? - - // Timestamps - var createdAt: Date - var lastUpdatedAt: Date - - // Relationships - var dataContract: PersistentDataContract? - - @Relationship(deleteRule: .cascade) - var balances: [PersistentTokenBalance]? - - @Relationship(deleteRule: .cascade) - var historyEvents: [PersistentTokenHistoryEvent]? - - init(contractId: Data, position: Int, name: String, baseSupply: String, decimals: Int = 8) { - // Create unique ID by combining contract ID and position - var idData = contractId - withUnsafeBytes(of: position.bigEndian) { bytes in - idData.append(contentsOf: bytes) - } - self.id = idData - - self.contractId = contractId - self.position = position - self.name = name - self.baseSupply = baseSupply - self.decimals = decimals - - // Default values - self.isPaused = false - self.allowTransferToFrozenBalance = true - self.keepsTransferHistory = true - self.keepsFreezingHistory = true - self.keepsMintingHistory = true - self.keepsBurningHistory = true - self.keepsDirectPricingHistory = true - self.keepsDirectPurchaseHistory = true - self.mintingAllowChoosingDestination = true - self.tradeMode = TokenTradeMode.notTradeable - - self.createdAt = Date() - self.lastUpdatedAt = Date() - } - } - - @Model - final class PersistentTokenBalance { - /// Index `networkRaw` for per-network balance scans. Token-balance - /// rows are aggregated per-identity per-token; UI surfaces always - /// scope to the active network. - #Index([\.networkRaw]) - - // MARK: - Core Properties - var tokenId: String - var identityId: Data - /// Schema-stable signed carrier for the protocol's unsigned balance. - /// SwiftData/SQLite keep the original `balance` Int64 column unchanged; - /// interpret its bits through `unsignedBalance` at every API boundary. - var balance: Int64 - var frozen: Bool - - // MARK: - Timestamps - var createdAt: Date - var lastUpdated: Date - var lastSyncedAt: Date? - - // MARK: - Token Info (Cached) - var tokenName: String? - var tokenSymbol: String? - var tokenDecimals: Int32? - - // MARK: - Network - /// Stored as the `Network.rawValue` `UInt32` so SwiftData - /// `#Predicate` expressions can evaluate it directly. See - /// `PersistentIdentity.networkRaw` for the full rationale. - var networkRaw: UInt32 - - /// Type-safe accessor over `networkRaw`. Setter writes through. - var network: Network { - get { Network(rawValue: networkRaw) ?? .testnet } - set { networkRaw = newValue.rawValue } - } - - // MARK: - Relationships - @Relationship(deleteRule: .nullify) var identity: PersistentIdentity? - @Relationship(inverse: \PersistentToken.balances) var token: PersistentToken? - - // MARK: - Initialization - init( - tokenId: String, - identityId: Data, - balance: Int64 = 0, - frozen: Bool = false, - tokenName: String? = nil, - tokenSymbol: String? = nil, - tokenDecimals: Int32? = nil, - network: Network - ) { - self.tokenId = tokenId - self.identityId = identityId - self.balance = balance - self.frozen = frozen - self.tokenName = tokenName - self.tokenSymbol = tokenSymbol - self.tokenDecimals = tokenDecimals - self.createdAt = Date() - self.lastUpdated = Date() - self.lastSyncedAt = nil - self.networkRaw = network.rawValue - } - - /// Full-domain unsigned initializer. The distinct argument label preserves - /// the original public `balance: Int64` source API without making integer - /// literals ambiguous between signed and unsigned overloads. - public convenience init( - tokenId: String, - identityId: Data, - unsignedBalance: UInt64, - frozen: Bool = false, - tokenName: String? = nil, - tokenSymbol: String? = nil, - tokenDecimals: Int32? = nil, - network: Network - ) { - self.init( - tokenId: tokenId, - identityId: identityId, - balance: Int64(bitPattern: unsignedBalance), - frozen: frozen, - tokenName: tokenName, - tokenSymbol: tokenSymbol, - tokenDecimals: tokenDecimals, - network: network - ) - } - - // MARK: - Computed Properties - /// Lossless full-domain view over the schema-stable signed carrier. - var unsignedBalance: UInt64 { - get { UInt64(bitPattern: balance) } - set { balance = Int64(bitPattern: newValue) } - } - - var formattedBalance: String { - let decimals: Int - if let tokenDecimals { - decimals = Int(tokenDecimals) - } else if let tokenDecimals = token?.decimals { - decimals = tokenDecimals - } else { - return "\(unsignedBalance)" - } - - guard decimals > 0 else { return String(unsignedBalance) } - - // Place the decimal point in the exact integer string. A Double - // conversion loses low digits well before UInt64.max. - let digits = String(unsignedBalance) - let scale = decimals - if digits.count <= scale { - return "0." + String(repeating: "0", count: scale - digits.count) + digits - } - let split = digits.index(digits.endIndex, offsetBy: -scale) - return String(digits[.. [String: Any]? { - guard let data = additionalDataJSON else { return nil } - return try? JSONSerialization.jsonObject(with: data) as? [String: Any] - } - } - - @Model - final class PersistentTransaction { - /// Index on `firstSeen` so per-wallet queries — which fetch - /// `PersistentTxo` rows by `walletId` then sort their parent - /// transactions by `firstSeen` — get a sorted scan instead of - /// an in-memory O(N log N) pass. The unique `txid` index covers - /// point-lookups; this one covers the timeline. - #Index([\.firstSeen]) - - /// Transaction ID (32-byte hash, raw little-endian wire bytes — - /// the same orientation Rust hands us via the FFI `[u8; 32]`). - /// Stored as raw `Data` so the unique index covers 32 bytes - /// instead of a 64-char hex string, and the persistence - /// handler avoids a hex round-trip on every write. - @Attribute(.unique) var txid: Data - /// Raw transaction bytes (consensus-encoded — the same wire - /// format `dashcore::consensus::encode::serialize` produces and - /// `Transaction::consensus_decode` round-trips). The FFI write - /// path always populates this; the persister-fallback read path - /// (`PlatformWalletPersistence::get_core_tx_record`) hands it - /// back over FFI so Rust can decode a real `Transaction` - /// without a placeholder body. - var transactionData: Data - /// Context: 0=mempool, 1=instantSend, 2=inBlock, 3=inChainLockedBlock. - var context: UInt32 - /// Block height (0 for mempool). - var blockHeight: UInt32 - /// Block hash (nil for mempool). - var blockHash: Data? - /// Block timestamp. - var blockTimestamp: UInt32 - /// The transaction's index within its block (`block.vtx` order), - /// meaningful only when [`hasBlockPosition`]. Pure storage of the - /// Rust-stamped value (rust-dashcore#891): restored provider special - /// transactions hand it back so the masternode aggregation keeps - /// Core's same-block apply order across restarts. `false` on rows - /// persisted before the field existed and on unconfirmed contexts. - var blockPosition: UInt32 = 0 - var hasBlockPosition: Bool = false - /// Direction: 0=incoming, 1=outgoing, 2=internal, 3=coinJoin. - var direction: UInt32 - /// Transaction type name (Standard, CoinJoin, etc.). Sourced - /// from Rust's `Debug` repr of `TransactionType` for human - /// display only — DO NOT use this string as a discriminant; - /// match on [`transactionTypeKind`] instead. The string is - /// not a stable wire contract (a `#[derive(Debug)]` rename on - /// the Rust side would silently change it). - var transactionType: String - /// Typed discriminant of Rust's - /// `key_wallet::transaction_checking::transaction_router::TransactionType`, - /// kept in lockstep with [`TransactionTypeKind`]. Use this byte - /// (via [`typedKind`] / [`isAssetLock`] / [`isAssetUnlock`]) to - /// branch on transaction kind in UI code; the parallel - /// [`transactionType`] string is human-readable only and not - /// stable. - /// - /// Sentinel `0xFF` means "pre-feature row whose discriminant - /// hasn't been populated yet" — SPV's next upsert round - /// replaces it with the real discriminant on touch. Accessors - /// treat the sentinel as unknown (no branch fires). - var transactionTypeKind: UInt8 = 0xFF - /// Net amount in duffs (signed: positive=received, negative=sent). - var netAmount: Int64 - /// Fee in duffs (nil if unknown). - var fee: UInt64? - /// User-assigned label. - var label: String - /// Timestamp when first observed (Unix seconds). - var firstSeen: UInt64 - - // MARK: - Provider (masternode) special-transaction payload - - /// Fields lifted by the Rust FFI from a ProRegTx / ProUpServTx - /// DIP-3 payload (see `provider_payload_fields` in - /// `rs-platform-wallet-ffi`). All optional — populated only when - /// [`typedKind`] is `.providerRegistration` / `.providerUpdateService`. - /// The Swift side never decodes the payload; these are pure storage. - /// - /// Masternode service endpoint as `"ip:port"`. - var providerServiceAddress: String? = nil - /// ProUpServTx `proTxHash` (32 raw wire bytes) linking the update to - /// its registration. `nil` for ProRegTx (whose own txid is the - /// proTxHash). - var providerProTxHash: Data? = nil - /// ProRegTx collateral outpoint txid (32 raw wire bytes); pair with - /// [`providerCollateralVout`]. `nil` when not a ProRegTx. - var providerCollateralTxid: Data? = nil - var providerCollateralVout: UInt32 = 0 - /// ProRegTx owner / voting key hashes (hash160, 20 bytes each). - var providerOwnerKeyHash: Data? = nil - var providerVotingKeyHash: Data? = nil - - /// Record timestamps. - var createdAt: Date - var lastUpdated: Date - - /// Transaction outputs created by this transaction. - /// - /// Cascade-deletes the matching `PersistentTxo` rows when the - /// transaction is removed — outputs cannot meaningfully exist - /// without their containing transaction (the outpoint, script, - /// amount, and address are all derived from it). - @Relationship(deleteRule: .cascade, inverse: \PersistentTxo.transaction) - var outputs: [PersistentTxo] = [] - - /// Transaction outputs spent *by* this transaction. - /// - /// Inverse of `PersistentTxo.spendingTransaction`. Default - /// `.nullify` delete rule (do not pass `.cascade`!) — those TXOs - /// are owned by their *creating* transaction, not this one. - /// Cascading from the spending side would let a recent tx wipe - /// outputs of an older tx on delete: a data-loss bug. Removing - /// this transaction merely detaches the spend-link and the TXOs - /// flip back to "unspent" until something else claims them. - @Relationship(inverse: \PersistentTxo.spendingTransaction) - var inputs: [PersistentTxo] = [] - - /// Pending input outpoints — entries this transaction's input - /// list references but for which no `PersistentTxo` has been - /// upserted yet. Filled by `PlatformWalletPersistenceHandler. - /// upsertTransaction` via the FFI's `input_outpoints` slice; - /// each entry is consumed (deleted) by `upsertUtxo` when the - /// matching previous-output finally arrives. See - /// `PersistentPendingInput` for the full reconciliation flow. - /// Cascade-delete: removing the spending tx drops every pending - /// row that hasn't resolved yet. - @Relationship(deleteRule: .cascade, inverse: \PersistentPendingInput.spendingTransaction) - var pendingInputs: [PersistentPendingInput] = [] - - /// Every account whose changeset bucket carried this tx record. - /// - /// This is a **superset** of the TXO-derived membership: it - /// includes payload-only involvement (special-tx payloads whose - /// Provider Owner / Voting key addresses matched an account) where - /// no `PersistentTxo` exists in the account, so the TXO join can - /// never surface it. The persistence handler appends the matched - /// account here for every record it upserts, mirroring how - /// `WalletChangeSetFFI::from_changeset` buckets `cs.records` by - /// `record.account_type` on the Rust side. - /// - /// The TXO join (`outputs` / `inputs` → `PersistentTxo.account`) - /// remains the canonical path for **funds** — balances, spend - /// tracking, per-address history all flow through it. This join - /// exists only so payload-only involvement is representable at - /// all; treat it as "account participation," not "account owns - /// value in this tx." - /// - /// Inverse of `PersistentAccount.involvedTransactions`, declared - /// on this side only (SwiftData needs the `inverse:` on exactly - /// one end of a many-to-many pair). Default `.nullify` delete rule - /// on both sides — deleting an account merely detaches it from the - /// tx (and vice versa); neither end cascades, since the tx row is - /// shared across accounts / wallets and the account outlives any - /// single tx. - @Relationship(inverse: \PersistentAccount.involvedTransactions) - var involvedAccounts: [PersistentAccount] = [] - - init( - txid: Data, - transactionData: Data, - context: UInt32 = 0, - blockHeight: UInt32 = 0, - direction: UInt32 = 0, - transactionType: String = "Standard", - netAmount: Int64 = 0, - firstSeen: UInt64 = 0 - ) { - self.txid = txid - self.transactionData = transactionData - self.context = context - self.blockHeight = blockHeight - self.blockTimestamp = 0 - self.direction = direction - self.transactionType = transactionType - self.netAmount = netAmount - self.firstSeen = firstSeen - self.label = "" - self.createdAt = Date() - self.lastUpdated = Date() - } - - // MARK: - Display Helpers - - /// Hex-encoded txid for UI / log sites. The on-disk row stores - /// the raw 32 bytes in wire/internal order (matches what - /// `dashcore::Txid::as_ref()` hands the FFI). The canonical - /// Bitcoin/Dash display convention is the *reverse* of those - /// bytes (the `Txid: Display` impl in dashcore-rust does the - /// same flip), so block-explorer hex matches what users see - /// here. Storage stays unflipped — predicate fetches compare - /// wire-order `Data` directly without re-encoding. - var txidHex: String { - txid.reversed().map { String(format: "%02x", $0) }.joined() - } - - var contextName: String { - switch context { - case 0: return "Mempool" - case 1: return "InstantSend" - case 2: return "In Block" - case 3: return "Chain Locked" - default: return "Unknown" - } - } - - var directionName: String { - switch direction { - case 0: return "Incoming" - case 1: return "Outgoing" - case 2: return "Internal" - case 3: return "CoinJoin" - default: return "Unknown" - } - } - - /// Typed view onto [`transactionTypeKind`]. `nil` only for the - /// `0xFF` sentinel (pre-feature row not yet re-persisted by SPV) - /// or for a future Rust-side variant addition Swift hasn't - /// learned about yet — both treated as "unknown" by the - /// `isAssetLock` / `isAssetUnlock` accessors so an unexpected - /// byte never silently fires the wrong branch. - var typedKind: TransactionTypeKind? { - TransactionTypeKind(rawValue: transactionTypeKind) - } - - /// `true` when this transaction is a Dash Platform asset-lock - /// funding tx — a Layer-1 burn that mints Layer-2 credits. The - /// wallet's `direction` classifier reports `Internal` because the - /// credit output is derived from this wallet's identity-funding - /// account, but the *intent* is conversion to L2 credits, not - /// "transaction to myself." - var isAssetLock: Bool { - typedKind == .assetLock - } - - /// Companion to [`isAssetLock`] — withdrawal back to L1. - var isAssetUnlock: Bool { - typedKind == .assetUnlock - } - - /// `true` for a masternode provider-registration (ProRegTx). - var isProviderRegistration: Bool { - typedKind == .providerRegistration - } - - /// `true` for a masternode provider-update-service (ProUpServTx). - var isProviderUpdateService: Bool { - typedKind == .providerUpdateService - } - - /// ProUpServTx proTxHash in block-explorer (reversed) hex, or `nil`. - /// Matches [`txidHex`]'s display-order convention. - var providerProTxHashHex: String? { - providerProTxHash.map { $0.reversed().map { String(format: "%02x", $0) }.joined() } - } - - /// ProRegTx collateral outpoint as `"txidHex:vout"` in display order, - /// or `nil` when there's no collateral field. - var providerCollateralDisplay: String? { - guard let txid = providerCollateralTxid else { return nil } - let hex = txid.reversed().map { String(format: "%02x", $0) }.joined() - return "\(hex):\(providerCollateralVout)" - } - - /// ProRegTx owner key hash (hash160) in hex — key hashes are shown - /// in their natural forward byte order, unlike txids. - var providerOwnerKeyHashHex: String? { - providerOwnerKeyHash.map { $0.map { String(format: "%02x", $0) }.joined() } - } - - /// ProRegTx voting key hash (hash160) in forward-order hex. - var providerVotingKeyHashHex: String? { - providerVotingKeyHash.map { $0.map { String(format: "%02x", $0) }.joined() } - } - - /// `true` for masternode provider special transactions (ProRegTx - /// and the three ProUp*Tx kinds). Like asset locks, these get - /// classified `Internal` by the wallet's direction logic (the - /// wallet only sees its own owner/voting/payout keys referenced - /// in the payload), so direction-derived labels like - /// "Self-Transfer" are misleading for them. - var isProviderSpecial: Bool { - providerSpecialName != nil - } - - /// Human-readable name for provider special transactions, `nil` - /// for every other kind. - var providerSpecialName: String? { - switch typedKind { - case .providerRegistration: return "Provider Registration" - case .providerUpdateRegistrar: return "Provider Update Registrar" - case .providerUpdateService: return "Provider Update Service" - case .providerUpdateRevocation: return "Provider Update Revocation" - default: return nil - } - } - - /// Direction text for UI surfaces, overridden for asset-lock / - /// asset-unlock txs (the L1 DASH isn't going "to myself" — it's - /// being converted to / from L2 platform credits) and for - /// provider special txs (the payload references our keys but no - /// value moves "to myself"). - /// - /// Use this anywhere a human-readable "what happened" label is - /// needed; fall back to [`directionName`] only when the consumer - /// genuinely needs the raw direction (e.g. the filter dropdown). - var displayDirection: String { - if isAssetLock { return "Asset Lock" } - if isAssetUnlock { return "Asset Unlock" } - if let name = providerSpecialName { return name } - return directionName - } - - var formattedAmount: String { - let dash = Double(abs(netAmount)) / 100_000_000.0 - let sign = netAmount >= 0 ? "+" : "-" - return String(format: "%@%.8f DASH", sign, dash) - } - } - - @Model - final class PersistentTxo { - /// Index `walletId` so per-wallet TXO scans — the canonical - /// "show every TXO (and, by union of `transaction` + - /// `spendingTransaction`, every transaction) that touches wallet - /// W" path — hit an index instead of scanning the entire TXO - /// table. The denorm is what makes the predicate translatable - /// to SQL in the first place; this just makes the resulting - /// query fast at scale. - #Index([\.walletId]) - - /// Outpoint: 36 raw bytes (32-byte txid in wire orientation + - /// 4-byte vout little-endian) — the standard Bitcoin outpoint - /// serialization. Unique identifier stored explicitly so - /// SwiftData predicate fetches can hit a single column without - /// traversing the `transaction` relationship. Always equals - /// `PersistentTxo.makeOutpoint(txid: transaction.txid, vout: vout)`. - @Attribute(.unique) var outpoint: Data - /// Output index within the transaction. - var vout: UInt32 - /// Value in duffs. - var amount: UInt64 - /// Owning address (Base58Check). - var address: String - /// Script pubkey bytes. - var scriptPubKey: Data - /// Block height where created. - var height: UInt32 - /// Whether this is a coinbase output. - var isCoinbase: Bool - /// Whether confirmed in a block. - var isConfirmed: Bool - /// Whether locked by InstantSend. - var isInstantLocked: Bool - /// Whether reserved/locked for a specific purpose. - var isLocked: Bool - /// Whether this TXO has been spent. - /// - /// Denormalized: should track `spendingTransaction != nil`. Kept - /// as an explicit column because per-row spent/unspent filters - /// are a hot query path, and chasing the optional relationship - /// in a predicate drops SwiftData onto the same nested-optional - /// codepath that crashes elsewhere. The persistence handler is - /// responsible for keeping the two in sync; do not enforce - /// invariants here. - var isSpent: Bool - /// Record timestamps. - var createdAt: Date - var lastUpdated: Date - - /// 32-byte wallet ID this TXO belongs to. Denormalized from - /// `account?.wallet.walletId` so per-wallet `@Query` predicates - /// can filter with a single equality check instead of chaining - /// through the optional `account` relationship — SwiftData's - /// predicate compiler can't translate that chain into SQLite and - /// crashes with `Unsupported function expression TERNARY(...).walletId`. - /// This is the single column callers filter on for "show every - /// TXO (and, by union of `transaction` + `spendingTransaction`, - /// every transaction) that touches wallet W". Empty `Data()` for - /// rows migrated from older schema; the next sync pass will - /// populate it. - var walletId: Data = Data() - - /// Containing transaction (the one that *created* this output). - /// Cascade-deleted from the parent side (see - /// `PersistentTransaction.outputs`). Optional only because the - /// underlying SwiftData inverse must allow nil during the brief - /// window between row insert and relationship attachment; in - /// steady state every TXO has a non-nil `transaction`. - var transaction: PersistentTransaction? - - /// The transaction that *spent* this output, or nil if the TXO - /// is unspent. Inverse of `PersistentTransaction.inputs`. Uses - /// the default `.nullify` delete rule from that side — deleting - /// the spending tx must not cascade-delete this row. - var spendingTransaction: PersistentTransaction? - - /// Position of this output within `spendingTransaction.input` - /// (i.e. the canonical "vin index"). Captured at the moment the - /// spend is reconciled — sourced from - /// `TransactionRecordFFI.input_outpoints` index, which itself - /// comes from `tx.input.iter()` on the Rust side, so the value - /// matches the serialized transaction's input ordering exactly. - /// `nil` when the TXO is unspent (no spending tx, no vin index) - /// or when migrated from an older row that predates the column. - /// Surfaced by `TransactionStorageDetailView` so input rows - /// render in serialized vin order with their real positions - /// rather than being re-sorted by outpoint hex (which loses - /// the relationship between row and serialized index). - var spendingInputIndex: UInt32? = nil - - /// Parent account. No longer paired with an inverse on the - /// account side — the canonical account path is - /// `coreAddress?.account`. This field is the fallback when the - /// address row isn't yet linked (out-of-order flush, address - /// pool rebuild, etc.). - var account: PersistentAccount? - - /// Owning `PersistentCoreAddress` row, if it exists in the - /// account's address pool. Linked alongside `address` (the - /// Base58Check string) — the string is the authoritative - /// identifier and survives even when the address pool is rebuilt - /// or the TXO was paid to an address never in our pool (e.g. an - /// outgoing recipient). The relationship is the convenient - /// pointer for navigating to derivation metadata, balance, and - /// pool tag without a separate fetch. Inverse of - /// `PersistentCoreAddress.txos`; `.cascade` on that side so - /// account / wallet teardown drops TXOs cleanly. - var coreAddress: PersistentCoreAddress? - - init( - transaction: PersistentTransaction, - vout: UInt32, - amount: UInt64, - address: String, - scriptPubKey: Data = Data(), - height: UInt32 = 0 - ) { - self.outpoint = Self.makeOutpoint(txid: transaction.txid, vout: vout) - self.vout = vout - self.amount = amount - self.address = address - self.scriptPubKey = scriptPubKey - self.height = height - self.isCoinbase = false - self.isConfirmed = false - self.isInstantLocked = false - self.isLocked = false - self.isSpent = false - self.createdAt = Date() - self.lastUpdated = Date() - self.transaction = transaction - } - - /// Build the 36-byte outpoint key (32-byte txid raw bytes + - /// 4-byte vout little-endian). Exposed so the persistence - /// handler can compose predicates / lookups directly from the - /// FFI's `[u8; 32]` + `u32` without going through string - /// formatting. - static func makeOutpoint(txid: Data, vout: UInt32) -> Data { - var data = Data(capacity: 36) - data.append(txid) - var v = vout.littleEndian - withUnsafeBytes(of: &v) { data.append(contentsOf: $0) } - return data - } - - /// Convenience accessor for the containing transaction's txid - /// as raw 32-byte `Data`. Prefers the `transaction` relationship; - /// falls back to the first 32 bytes of `outpoint` when the - /// inverse is briefly nil during insert (so storage-explorer - /// rows still render a stable identifier rather than collapsing - /// to empty). - var txid: Data { - if let transaction { - return transaction.txid - } - return outpoint.count >= 32 ? Data(outpoint.prefix(32)) : Data() - } - - /// Hex-encoded txid for UI / log sites. Reverses bytes to match - /// the canonical block-explorer display (same flip as - /// `dashcore::Txid: Display`). Mirrors - /// `PersistentTransaction.txidHex` directly so the two stay in - /// sync; can't simply forward to it because we want the same - /// hex even when `transaction` is briefly unattached. - var txidHex: String { - let rawTxid = txid - guard rawTxid.count == 32 else { return "" } - return rawTxid.reversed().map { String(format: "%02x", $0) }.joined() - } - - /// Human-readable outpoint (`:`) for UI / log - /// sites. Reconstructs from `txidHex` so the byte-flip stays - /// consistent across all display surfaces. - var outpointHex: String { - let hex = txidHex - return hex.isEmpty ? "" : "\(hex):\(vout)" - } - - var formattedAmount: String { - let dash = Double(amount) / 100_000_000.0 - return String(format: "%.8f DASH", dash) - } - } - - @Model - final class PersistentWallet { - /// Index `networkRaw` so per-network wallet scans (used everywhere - /// from the network-scoped storage explorer to the per-network - /// "is there a wallet on this chain yet" lookups) don't degrade - /// to a table scan. Also index `walletGroupId` so the Wallet Info - /// "Networks" lookup — which fetches every sibling-network row for - /// a seed by its group id — stays a keyed scan. - #Index([\.networkRaw], [\.walletGroupId]) - #Unique([\.walletId]) - - /// 32-byte NETWORK-SCOPED wallet ID, and the row's primary - /// uniqueness key. Since the network-scoping change the same seed - /// yields a DISTINCT `walletId` per network (a domain-tagged network - /// byte is folded into the digest), so a wallet that exists on - /// multiple chains has one row per network, each with its own id — - /// the network is already baked into the id, so `walletId` alone is - /// globally unique (an earlier `(walletId, networkRaw)` composite - /// was a leftover from the pre-scoping model, where one seed shared - /// a single id across networks and `networkRaw` was the only - /// distinguishing column). To gather a seed's sibling-network rows, - /// group by `walletGroupId` (which is the same across networks), - /// not by this id. - var walletId: Data - /// 32-byte NETWORK-INDEPENDENT group id shared by every network's - /// wallet derived from the same seed (Rust computes it as the - /// no-network digest of the root key). Distinct from `walletId`, - /// which is network-scoped. Used to group a seed's sibling-network - /// rows in the Wallet Info "Networks" section. Defaults to empty - /// for rows written before this column existed (pre-release, no - /// migration); consumers treat empty as "legacy — this single row - /// only". - var walletGroupId: Data = Data() - /// Network this wallet belongs to. `nil` means "not yet known" — - /// the row was created by a changeset before `persistWalletMetadata` - /// filled the network in. Views treat `nil` as unknown. - /// - /// Stored as the `Network.rawValue` `UInt32?` so SwiftData - /// `#Predicate` expressions can evaluate it directly. See - /// `PersistentIdentity.networkRaw` for the full rationale. - var networkRaw: UInt32? - - /// Type-safe accessor over `networkRaw`. `nil` round-trips as - /// `nil`; non-nil reads fall back to `.testnet` if the stored - /// raw value ever drifts out of the `Network` range. - var network: Network? { - get { - guard let raw = networkRaw else { return nil } - return Network(rawValue: raw) ?? .testnet - } - set { networkRaw = newValue?.rawValue } - } - /// Optional wallet name. - var name: String? - /// Optional free-form user-supplied description. Mirrored into - /// the keychain metadata blob (see `WalletKeychainMetadata`) so - /// it survives a SwiftData wipe / reinstall via the - /// orphan-mnemonic recovery flow. No UI surfaces this yet, but - /// the column is wired so existing rows roll forward without a - /// schema migration when it lands. - var walletDescription: String? - /// Birth height — block height when the wallet was created. - var birthHeight: UInt32 - /// Last synced core block height. - var syncedHeight: UInt32 - /// Timestamp of last sync (Unix seconds). - var lastSynced: UInt64 - /// Bincode-serialised - /// `dashcore::ephemerealdata::chain_lock::ChainLock` carrying the - /// wallet's `WalletMetadata::last_applied_chain_lock` from the - /// previous session. Roundtripped across app launches so the - /// asset-lock-resume CL-from-metadata fallback in Rust's - /// `proof.rs` can fire on catch-up at launch without waiting - /// for SPV to re-apply a fresh ChainLock. `nil` when no - /// ChainLock has ever been observed for this wallet (fresh - /// wallet, or pre-feature row). - var lastAppliedChainLockBytes: Data? - /// User imported this wallet from an existing mnemonic (as - /// opposed to generating a fresh one). Cosmetic flag that - /// drives the "📥 Imported" badge; defaulted to `false` for - /// rows that predate the column. - var isImported: Bool = false - /// Verified seed-binding marker: the BIP44 account-0 xpub that the - /// Keychain-resolved seed was proven to derive, bound to the mnemonic - /// Keychain item's identity stamp, written after one successful - /// `platform_wallet_verify_seed_binds_to_wallet_cached` run. On later - /// launches the unlock path hands this back to Rust (with the item's - /// current stamp), which skips the mnemonic-resolving derivation when - /// it still matches — and re-verifies when the xpub OR the Keychain - /// item changed. Opaque to Swift — Rust decides match-vs-verify; this - /// column only stores and returns it. `nil` (rows predating the - /// column, or never verified) means the full check runs at the next - /// unlock. - var seedBindingVerifiedMarker: String? - /// Record timestamps. - var createdAt: Date - var lastUpdated: Date - - /// Accounts belonging to this wallet. - @Relationship(deleteRule: .cascade, inverse: \PersistentAccount.wallet) - var accounts: [PersistentAccount] - - /// Identities registered against this wallet. Cardinality is - /// 0..N — a wallet may have zero identities (freshly created) - /// or many. Deletion semantics: `.nullify` so an identity - /// survives a wallet delete as an orphaned row (useful for - /// post-mortem inspection and possible re-association if the - /// wallet is re-imported from the same seed). - /// - /// Paired with `PersistentIdentity.wallet` (plain stored - /// property; the inverse key lives on this side). - @Relationship(deleteRule: .nullify, inverse: \PersistentIdentity.wallet) - var identities: [PersistentIdentity] - - init( - walletId: Data, - walletGroupId: Data = Data(), - network: Network? = nil, - name: String? = nil, - walletDescription: String? = nil, - birthHeight: UInt32 = 0, - syncedHeight: UInt32 = 0, - isImported: Bool = false - ) { - self.walletId = walletId - self.walletGroupId = walletGroupId - self.networkRaw = network?.rawValue - self.name = name - self.walletDescription = walletDescription - self.birthHeight = birthHeight - self.syncedHeight = syncedHeight - self.lastSynced = 0 - self.isImported = isImported - self.createdAt = Date() - self.lastUpdated = Date() - self.accounts = [] - self.identities = [] - } - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAccount.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAccount.swift new file mode 100644 index 00000000000..3cababb0805 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAccount.swift @@ -0,0 +1,78 @@ +import Foundation +import SwiftData + +// `PersistentAccount` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentAccount { + #Unique([ + \.wallet, + \.accountType, + \.accountIndex, + \.standardTag, + \.registrationIndex, + \.keyClass, + \.userIdentityId, + \.friendIdentityId, + ]) + + var accountType: UInt32 + var accountIndex: UInt32 + var accountTypeName: String + var balanceConfirmed: UInt64 + var balanceUnconfirmed: UInt64 + var externalHighestUsed: Int32 + var internalHighestUsed: Int32 + var standardTag: UInt8 + var registrationIndex: UInt32 + var keyClass: UInt32 + var userIdentityId: Data + var friendIdentityId: Data + @Attribute(.unique) var accountExtendedPubKeyBytes: Data? + var createdAt: Date + var lastUpdated: Date + + var wallet: PersistentWallet + + @Relationship(deleteRule: .cascade, inverse: \PersistentCoreAddress.account) + var coreAddresses: [PersistentCoreAddress] + + @Relationship(deleteRule: .cascade, inverse: \PersistentPlatformAddress.account) + var platformAddresses: [PersistentPlatformAddress] + + var involvedTransactions: [PersistentTransaction] = [] + + init( + wallet: PersistentWallet, + accountType: UInt32, + accountIndex: UInt32, + accountTypeName: String + ) { + self.wallet = wallet + self.accountType = accountType + self.accountIndex = accountIndex + self.accountTypeName = accountTypeName + self.balanceConfirmed = 0 + self.balanceUnconfirmed = 0 + self.externalHighestUsed = -1 + self.internalHighestUsed = -1 + self.standardTag = 0 + self.registrationIndex = 0 + self.keyClass = 0 + self.userIdentityId = Data() + self.friendIdentityId = Data() + self.accountExtendedPubKeyBytes = nil + self.createdAt = Date() + self.lastUpdated = Date() + self.coreAddresses = [] + self.platformAddresses = [] + self.involvedTransactions = [] + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAssetLock.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAssetLock.swift new file mode 100644 index 00000000000..233959f2991 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAssetLock.swift @@ -0,0 +1,100 @@ +import Foundation +import SwiftData + +// `PersistentAssetLock` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 7127c38566. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentAssetLock { + #Index([\.walletId]) + + @Attribute(.unique) var outPointHex: String + + var walletId: Data + + var transactionBytes: Data + + var fundingTypeRaw: Int + + var identityIndexRaw: Int32 + + var accountIndexRaw: Int32 = 0 + + var amountDuffs: Int64 + + var statusRaw: Int + + var proofBytes: Data? + + var recipientPlatformAddressHash: Data? + + var recipientPlatformAddressType: UInt8? + + var createdAt: Date + var updatedAt: Date + + init( + outPointHex: String, + walletId: Data, + transactionBytes: Data, + fundingTypeRaw: Int, + identityIndexRaw: Int32, + accountIndexRaw: Int32 = 0, + amountDuffs: Int64, + statusRaw: Int, + proofBytes: Data? = nil + ) { + self.outPointHex = outPointHex + self.walletId = walletId + self.transactionBytes = transactionBytes + self.fundingTypeRaw = fundingTypeRaw + self.identityIndexRaw = identityIndexRaw + self.accountIndexRaw = accountIndexRaw + self.amountDuffs = amountDuffs + self.statusRaw = statusRaw + self.proofBytes = proofBytes + self.createdAt = Date() + self.updatedAt = Date() + } + } +} + +extension DashSchemaV1.PersistentAssetLock { + static func predicate(walletId: Data) -> Predicate { + #Predicate { entry in + entry.walletId == walletId + } + } + + static func predicate( + walletId: Data, + identityIndex: UInt32 + ) -> Predicate { + let identityIndexRaw = Int32(bitPattern: identityIndex) + return #Predicate { entry in + entry.walletId == walletId && entry.identityIndexRaw == identityIndexRaw + } + } +} + +extension DashSchemaV1.PersistentAssetLock { + static func encodeOutPoint(rawBytes: Data) -> String { + precondition(rawBytes.count == 36, "outpoint must be 36 bytes") + let txid = rawBytes.prefix(32) + let voutBytes = rawBytes.suffix(4) + let vout = voutBytes.withUnsafeBytes { raw -> UInt32 in + var value: UInt32 = 0 + withUnsafeMutableBytes(of: &value) { dst in + dst.copyBytes(from: raw.prefix(MemoryLayout.size)) + } + return UInt32(littleEndian: value) + } + let txidHex = txid.reversed().map { String(format: "%02x", $0) }.joined() + return "\(txidHex):\(vout)" + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentCoreAddress.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentCoreAddress.swift new file mode 100644 index 00000000000..c13d9ddc403 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentCoreAddress.swift @@ -0,0 +1,68 @@ +import Foundation +import SwiftData + +// `PersistentCoreAddress` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentCoreAddress { + @Attribute(.unique) var address: String + var publicKey: Data + var keyType: UInt8 = 0 + var poolTypeTag: UInt8 + var addressIndex: UInt32 + var derivationPath: String + var isUsed: Bool + var firstSeenHeight: UInt32 + var lastSeenHeight: UInt32 + var balance: UInt64 + var createdAt: Date + var lastUpdated: Date + + var account: PersistentAccount? + + @Relationship(deleteRule: .cascade, inverse: \PersistentTxo.coreAddress) + var txos: [PersistentTxo] = [] + + init( + address: String, + publicKey: Data = Data(), + keyType: UInt8 = 0, + poolTypeTag: UInt8, + addressIndex: UInt32, + derivationPath: String, + isUsed: Bool = false, + balance: UInt64 = 0 + ) { + self.address = address + self.publicKey = publicKey + self.keyType = keyType + self.poolTypeTag = poolTypeTag + self.addressIndex = addressIndex + self.derivationPath = derivationPath + self.isUsed = isUsed + self.firstSeenHeight = 0 + self.lastSeenHeight = 0 + self.balance = balance + self.createdAt = Date() + self.lastUpdated = Date() + } + } +} + +extension DashSchemaV1.PersistentCoreAddress { + var poolTypeName: String { + switch poolTypeTag { + case 0: return "External" + case 1: return "Internal" + case 2: return "Additional" + case 3: return "Additional (Hardened)" + default: return "Unknown(\(poolTypeTag))" + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDPNSName.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDPNSName.swift new file mode 100644 index 00000000000..0f67290ab1f --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDPNSName.swift @@ -0,0 +1,132 @@ +import Foundation +import SwiftData + +// `PersistentDPNSName` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentDPNSName { + #Unique([\.networkRaw, \.normalizedParentDomainName, \.normalizedLabel]) + + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + var label: String + + var normalizedLabel: String + + var parentDomainName: String + + var normalizedParentDomainName: String + + var acquiredAt: UInt64 + + var isOwned: Bool = true + + var documentIdBase58: String? + + var priceCredits: Int64? + + var saleStatusRaw: Int16 = 0 + + var counterpartyIdBase58: String? + + var documentCreatedAtMs: UInt64? + + var documentUpdatedAtMs: UInt64? + + var documentTransferredAtMs: UInt64? + + var marketplaceUpdatedAt: UInt64 = 0 + + var identity: PersistentIdentity + + var createdAt: Date + var lastUpdated: Date + + init( + identity: PersistentIdentity, + label: String, + parentDomainName: String = "dash", + acquiredAt: UInt64 = 0, + isOwned: Bool = true + ) { + self.identity = identity + self.networkRaw = identity.networkRaw + self.label = label + self.normalizedLabel = Self.normalize(label) + self.parentDomainName = parentDomainName + self.normalizedParentDomainName = Self.normalize(parentDomainName) + self.acquiredAt = acquiredAt + self.isOwned = isOwned + self.documentIdBase58 = nil + self.priceCredits = nil + self.saleStatusRaw = 0 + self.counterpartyIdBase58 = nil + self.documentCreatedAtMs = nil + self.documentUpdatedAtMs = nil + self.documentTransferredAtMs = nil + self.marketplaceUpdatedAt = 0 + self.createdAt = Date() + self.lastUpdated = Date() + } + } +} + +extension DashSchemaV1.PersistentDPNSName { + var saleStatus: DpnsNameSaleStatus? { + guard documentIdBase58 != nil else { return nil } + switch saleStatusRaw { + case 0: + return .owned + case 1: + guard let to = counterpartyId else { return nil } + return .sold(to: to) + case 2: + guard let to = counterpartyId else { return nil } + return .transferred(to: to) + default: + return nil + } + } + + var counterpartyId: Data? { + counterpartyIdBase58.flatMap { Data.identifier(fromBase58: $0) } + } + + var listedPriceCredits: UInt64? { + guard documentIdBase58 != nil, let priceCredits else { return nil } + return UInt64(bitPattern: priceCredits) + } +} + +extension DashSchemaV1.PersistentDPNSName { + static func normalize(_ input: String) -> String { + String(input.map { c -> Character in + switch c { + case "o", "O": return "0" + case "i", "I": return "1" + case "l", "L": return "1" + default: return Character(c.lowercased()) + } + }) + } +} + +extension DashSchemaV1.PersistentDPNSName { + static func predicate(identityId: Data) -> Predicate { + let target = identityId + return #Predicate { name in + name.identity.identityId == target && name.isOwned == true + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactProfile.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactProfile.swift new file mode 100644 index 00000000000..155a68ce444 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactProfile.swift @@ -0,0 +1,97 @@ +import Foundation +import SwiftData + +// `PersistentDashpayContactProfile` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentDashpayContactProfile { + #Unique([ + \.networkRaw, \.ownerIdentityId, \.contactIdentityId + ]) + + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + var ownerIdentityId: Data + + var contactIdentityId: Data + + var displayName: String? + + var publicMessage: String? + + var bio: String? + + var avatarUrl: String? + + var avatarHash: Data? + + var avatarFingerprint: Data? + + var checkedAtMs: UInt64 + + var owner: PersistentIdentity + + var createdAt: Date + var lastUpdated: Date + + init( + owner: PersistentIdentity, + contactIdentityId: Data, + checkedAtMs: UInt64, + displayName: String? = nil, + publicMessage: String? = nil, + bio: String? = nil, + avatarUrl: String? = nil, + avatarHash: Data? = nil, + avatarFingerprint: Data? = nil + ) { + self.owner = owner + self.networkRaw = owner.networkRaw + self.ownerIdentityId = owner.identityId + self.contactIdentityId = contactIdentityId + self.checkedAtMs = checkedAtMs + self.displayName = displayName + self.publicMessage = publicMessage + self.bio = bio + self.avatarUrl = avatarUrl + self.avatarHash = avatarHash + self.avatarFingerprint = avatarFingerprint + self.createdAt = Date() + self.lastUpdated = Date() + } + } +} + +extension DashSchemaV1.PersistentDashpayContactProfile { + static func predicate( + ownerIdentityId: Data + ) -> Predicate { + let target = ownerIdentityId + return #Predicate { row in + row.ownerIdentityId == target + } + } + + static func predicate( + ownerIdentityId: Data, + contactIdentityId: Data + ) -> Predicate { + let target = ownerIdentityId + let contact = contactIdentityId + return #Predicate { row in + row.ownerIdentityId == target + && row.contactIdentityId == contact + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactRequest.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactRequest.swift new file mode 100644 index 00000000000..75aa5f3b4ee --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactRequest.swift @@ -0,0 +1,118 @@ +import Foundation +import SwiftData + +// `PersistentDashpayContactRequest` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentDashpayContactRequest { + #Unique([ + \.networkRaw, \.ownerIdentityId, \.contactIdentityId, \.isOutgoing + ]) + + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + var ownerIdentityId: Data + + var contactIdentityId: Data + + var isOutgoing: Bool + + var senderKeyIndex: UInt32 + + var recipientKeyIndex: UInt32 + + var accountReference: UInt32 + + var encryptedPublicKey: Data + + var encryptedAccountLabel: Data? + + var autoAcceptProof: Data? + + var coreHeightCreatedAt: UInt32 + + var createdAtMillis: UInt64 + + var paymentChannelBroken: Bool = false + + var contactAlias: String? + + var contactNote: String? + + var contactHidden: Bool = false + + var contactAccountLabel: String? + + var contactAcceptedAccounts: [UInt32] = [] + + var owner: PersistentIdentity + + var createdAt: Date + var lastUpdated: Date + + init( + owner: PersistentIdentity, + contactIdentityId: Data, + isOutgoing: Bool, + senderKeyIndex: UInt32, + recipientKeyIndex: UInt32, + accountReference: UInt32, + encryptedPublicKey: Data, + encryptedAccountLabel: Data? = nil, + autoAcceptProof: Data? = nil, + coreHeightCreatedAt: UInt32, + createdAtMillis: UInt64, + paymentChannelBroken: Bool = false + ) { + self.owner = owner + self.networkRaw = owner.networkRaw + self.ownerIdentityId = owner.identityId + self.contactIdentityId = contactIdentityId + self.isOutgoing = isOutgoing + self.senderKeyIndex = senderKeyIndex + self.recipientKeyIndex = recipientKeyIndex + self.accountReference = accountReference + self.encryptedPublicKey = encryptedPublicKey + self.encryptedAccountLabel = encryptedAccountLabel + self.autoAcceptProof = autoAcceptProof + self.coreHeightCreatedAt = coreHeightCreatedAt + self.createdAtMillis = createdAtMillis + self.paymentChannelBroken = paymentChannelBroken + self.createdAt = Date() + self.lastUpdated = Date() + } + } +} + +extension DashSchemaV1.PersistentDashpayContactRequest { + static func predicate( + ownerIdentityId: Data + ) -> Predicate { + let target = ownerIdentityId + return #Predicate { row in + row.ownerIdentityId == target + } + } + + static func predicate( + ownerIdentityId: Data, + isOutgoing: Bool + ) -> Predicate { + let target = ownerIdentityId + let direction = isOutgoing + return #Predicate { row in + row.ownerIdentityId == target && row.isOutgoing == direction + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayIgnoredSender.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayIgnoredSender.swift new file mode 100644 index 00000000000..242fbf1027d --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayIgnoredSender.swift @@ -0,0 +1,67 @@ +import Foundation +import SwiftData + +// `PersistentDashpayIgnoredSender` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentDashpayIgnoredSender { + #Unique([ + \.networkRaw, \.ownerIdentityId, \.ignoredSenderId + ]) + + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + var ownerIdentityId: Data + + var ignoredSenderId: Data + + var owner: PersistentIdentity + + var ignoredAt: Date + + init( + owner: PersistentIdentity, + ignoredSenderId: Data + ) { + self.owner = owner + self.networkRaw = owner.networkRaw + self.ownerIdentityId = owner.identityId + self.ignoredSenderId = ignoredSenderId + self.ignoredAt = Date() + } + } +} + +extension DashSchemaV1.PersistentDashpayIgnoredSender { + static func predicate( + ownerIdentityId: Data + ) -> Predicate { + let target = ownerIdentityId + return #Predicate { row in + row.ownerIdentityId == target + } + } + + static func predicate( + ownerIdentityId: Data, + ignoredSenderId: Data + ) -> Predicate { + let target = ownerIdentityId + let sender = ignoredSenderId + return #Predicate { row in + row.ownerIdentityId == target + && row.ignoredSenderId == sender + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayPayment.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayPayment.swift new file mode 100644 index 00000000000..319c209082f --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayPayment.swift @@ -0,0 +1,99 @@ +import Foundation +import SwiftData + +// `PersistentDashpayPayment` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentDashpayPayment { + #Unique([ + \.networkRaw, \.ownerIdentityId, \.txid + ]) + + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + var ownerIdentityId: Data + + var counterpartyIdentityId: Data + + var amountDuffs: UInt64 + + var directionRaw: UInt8 + + var direction: DashPayPaymentDirection { + get { DashPayPaymentDirection(rawValue: directionRaw) ?? .sent } + set { directionRaw = newValue.rawValue } + } + + var statusRaw: UInt8 + + var status: DashPayPaymentStatus { + get { DashPayPaymentStatus(rawValue: statusRaw) ?? .pending } + set { statusRaw = newValue.rawValue } + } + + var txid: String + + var memo: String? + + var owner: PersistentIdentity + + var createdAt: Date + var lastUpdated: Date + + init( + owner: PersistentIdentity, + counterpartyIdentityId: Data, + amountDuffs: UInt64, + direction: DashPayPaymentDirection, + status: DashPayPaymentStatus, + txid: String, + memo: String? = nil + ) { + self.owner = owner + self.networkRaw = owner.networkRaw + self.ownerIdentityId = owner.identityId + self.counterpartyIdentityId = counterpartyIdentityId + self.amountDuffs = amountDuffs + self.directionRaw = direction.rawValue + self.statusRaw = status.rawValue + self.txid = txid + self.memo = memo + self.createdAt = Date() + self.lastUpdated = Date() + } + } +} + +extension DashSchemaV1.PersistentDashpayPayment { + static func predicate( + ownerIdentityId: Data + ) -> Predicate { + let target = ownerIdentityId + return #Predicate { row in + row.ownerIdentityId == target + } + } + + static func predicate( + ownerIdentityId: Data, + counterpartyIdentityId: Data + ) -> Predicate { + let target = ownerIdentityId + let counterparty = counterpartyIdentityId + return #Predicate { row in + row.ownerIdentityId == target + && row.counterpartyIdentityId == counterparty + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayProfile.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayProfile.swift new file mode 100644 index 00000000000..9eec0834195 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayProfile.swift @@ -0,0 +1,70 @@ +import Foundation +import SwiftData + +// `PersistentDashpayProfile` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentDashpayProfile { + #Unique([\.networkRaw, \.identity]) + + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + var displayName: String? + + var publicMessage: String? + + var bio: String? + + var avatarUrl: String? + + var avatarHash: Data? + + var avatarFingerprint: Data? + + var identity: PersistentIdentity + + var createdAt: Date + var lastUpdated: Date + + init( + identity: PersistentIdentity, + displayName: String? = nil, + publicMessage: String? = nil, + bio: String? = nil, + avatarUrl: String? = nil, + avatarHash: Data? = nil, + avatarFingerprint: Data? = nil + ) { + self.identity = identity + self.networkRaw = identity.networkRaw + self.displayName = displayName + self.publicMessage = publicMessage + self.bio = bio + self.avatarUrl = avatarUrl + self.avatarHash = avatarHash + self.avatarFingerprint = avatarFingerprint + self.createdAt = Date() + self.lastUpdated = Date() + } + } +} + +extension DashSchemaV1.PersistentDashpayProfile { + static func predicate(identityId: Data) -> Predicate { + let target = identityId + return #Predicate { profile in + profile.identity.identityId == target + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDataContract.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDataContract.swift new file mode 100644 index 00000000000..302be50b557 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDataContract.swift @@ -0,0 +1,286 @@ +import Foundation +import SwiftData + +// `PersistentDataContract` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentDataContract { + #Index([\.networkRaw]) + + @Attribute(.unique) var id: Data + var name: String + var serializedContract: Data + var createdAt: Date + var lastAccessedAt: Date + + var binarySerialization: Data? + + var version: Int? + var ownerId: Data? + + @Relationship(deleteRule: .cascade, inverse: \PersistentKeyword.dataContract) + var keywordRelations: [PersistentKeyword] + var contractDescription: String? + + var schemaData: Data + var documentTypesData: Data + + var groupsData: Data? + + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + var lastUpdated: Date + var lastSyncedAt: Date? + + var canBeDeleted: Bool + var readonly: Bool + var keepsHistory: Bool + var schemaDefs: Int? + + var documentsKeepHistoryContractDefault: Bool + var documentsMutableContractDefault: Bool + var documentsCanBeDeletedContractDefault: Bool + + @Relationship(deleteRule: .cascade, inverse: \PersistentToken.dataContract) + var tokens: [PersistentToken]? + + @Relationship(deleteRule: .cascade, inverse: \PersistentDocumentType.dataContract) + var documentTypes: [PersistentDocumentType]? + + @Relationship(deleteRule: .cascade, inverse: \PersistentDocument.dataContract) + var documents: [PersistentDocument] + + @Relationship(deleteRule: .nullify, inverse: \PersistentIdentity.ownedDataContracts) + var ownerIdentity: PersistentIdentity? + + var hasTokens: Bool + var tokensData: Data? + + var idBase58: String { + id.toBase58String() + } + + var ownerIdBase58: String? { + ownerId?.toBase58String() + } + + var parsedContract: [String: Any]? { + try? JSONSerialization.jsonObject(with: serializedContract, options: []) as? [String: Any] + } + + var binarySerializationHex: String? { + binarySerialization?.toHexString() + } + + var keywords: [String] { + keywordRelations.map { $0.keyword } + } + + var schema: [String: Any] { + get { + guard let json = try? JSONSerialization.jsonObject(with: schemaData), + let dict = json as? [String: Any] else { + return [:] + } + return dict + } + set { + schemaData = (try? JSONSerialization.data(withJSONObject: newValue)) ?? Data() + lastUpdated = Date() + } + } + + var documentTypesList: [String] { + get { + guard let json = try? JSONSerialization.jsonObject(with: documentTypesData), + let array = json as? [String] else { + return [] + } + return array + } + set { + documentTypesData = (try? JSONSerialization.data(withJSONObject: newValue)) ?? Data() + lastUpdated = Date() + } + } + + var tokenConfigurations: [String: Any]? { + get { + guard let data = tokensData, + let json = try? JSONSerialization.jsonObject(with: data), + let dict = json as? [String: Any] else { + return nil + } + return dict + } + set { + if let newValue = newValue { + tokensData = try? JSONSerialization.data(withJSONObject: newValue) + hasTokens = true + } else { + tokensData = nil + hasTokens = false + } + lastUpdated = Date() + } + } + + var groups: [String: Any]? { + get { + guard let data = groupsData, + let json = try? JSONSerialization.jsonObject(with: data), + let dict = json as? [String: Any] else { + return nil + } + return dict + } + set { + if let newValue = newValue { + groupsData = try? JSONSerialization.data(withJSONObject: newValue) + } else { + groupsData = nil + } + lastUpdated = Date() + } + } + + init( + id: Data, + name: String, + serializedContract: Data, + version: Int? = 1, + ownerId: Data? = nil, + schema: [String: Any] = [:], + documentTypesList: [String] = [], + keywords: [String] = [], + description: String? = nil, + hasTokens: Bool = false, + network: Network + ) { + self.id = id + self.name = name + self.serializedContract = serializedContract + self.createdAt = Date() + self.lastAccessedAt = Date() + self.version = version + self.ownerId = ownerId + + self.schemaData = (try? JSONSerialization.data(withJSONObject: schema)) ?? Data() + self.documentTypesData = (try? JSONSerialization.data(withJSONObject: documentTypesList)) ?? Data() + + self.keywordRelations = keywords.map { PersistentKeyword(keyword: $0, contractId: id.toBase58String()) } + self.contractDescription = description + + self.hasTokens = hasTokens + self.tokensData = nil + + self.groupsData = nil + + self.documents = [] + + self.ownerIdentity = nil + + self.networkRaw = network.rawValue + self.lastUpdated = Date() + self.lastSyncedAt = nil + + self.canBeDeleted = false + self.readonly = false + self.keepsHistory = false + self.documentsKeepHistoryContractDefault = false + self.documentsMutableContractDefault = true + self.documentsCanBeDeletedContractDefault = true + } + + func updateLastAccessed() { + self.lastAccessedAt = Date() + } + + func updateVersion(_ newVersion: Int) { + self.version = newVersion + self.lastUpdated = Date() + } + + func markAsSynced() { + self.lastSyncedAt = Date() + } + + func addDocument(_ document: PersistentDocument) { + documents.append(document) + lastUpdated = Date() + } + + func removeDocument(withId documentId: String) { + if let docIdData = Data.identifier(fromBase58: documentId) { + documents.removeAll { $0.id == docIdData } + } + lastUpdated = Date() + } + } +} + +extension DashSchemaV1.PersistentDataContract { + static func predicate(contractId: String) -> Predicate { + guard let idData = Data.identifier(fromBase58: contractId) else { + return #Predicate { _ in false } + } + return #Predicate { contract in + contract.id == idData + } + } + + static func predicate(ownerId: Data) -> Predicate { + #Predicate { contract in + contract.ownerId == ownerId + } + } + + static func predicate(name: String) -> Predicate { + #Predicate { contract in + contract.name.localizedStandardContains(name) + } + } + + static var contractsWithTokensPredicate: Predicate { + #Predicate { contract in + contract.hasTokens == true + } + } + + static func predicate(keyword: String) -> Predicate { + #Predicate { contract in + contract.keywordRelations.contains { $0.keyword == keyword } + } + } + + static func needsSyncPredicate(olderThan date: Date) -> Predicate { + #Predicate { contract in + contract.lastSyncedAt == nil || contract.lastSyncedAt! < date + } + } + + static func predicate(network: Network) -> Predicate { + let target = network.rawValue + return #Predicate { contract in + contract.networkRaw == target + } + } + + static func contractsWithTokensPredicate(network: Network) -> Predicate { + let target = network.rawValue + return #Predicate { contract in + contract.hasTokens == true && contract.networkRaw == target + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocument.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocument.swift new file mode 100644 index 00000000000..6d9cc9b32f1 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocument.swift @@ -0,0 +1,181 @@ +import Foundation +import SwiftData + +// `PersistentDocument` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentDocument { + #Index([\.networkRaw]) + + @Attribute(.unique) var documentId: String + + var documentType: String + var revision: Int32 + var data: Data + + var contractId: String + var ownerId: String + + var contractIdData: Data + var ownerIdData: Data + + var createdAt: Date + var updatedAt: Date + var transferredAt: Date? + + var createdAtBlockHeight: Int64? + var updatedAtBlockHeight: Int64? + var transferredAtBlockHeight: Int64? + + var createdAtCoreBlockHeight: Int64? + var updatedAtCoreBlockHeight: Int64? + var transferredAtCoreBlockHeight: Int64? + + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + var isDeleted: Bool = false + + var localCreatedAt: Date + var localUpdatedAt: Date + + var documentType_relation: PersistentDocumentType? + var dataContract: PersistentDataContract? + + var ownerIdentity: PersistentIdentity? + + var id: Data { + Data.identifier(fromBase58: documentId) ?? Data() + } + + var idBase58: String { + documentId + } + + var ownerIdBase58: String { + ownerId + } + + var contractIdBase58: String { + contractId + } + + var properties: [String: Any]? { + try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } + + var displayTitle: String { + guard let props = properties else { return "Document" } + + if let title = props["title"] as? String { return title } + if let name = props["name"] as? String { return name } + if let label = props["label"] as? String { return label } + if let normalizedLabel = props["normalizedLabel"] as? String { return normalizedLabel } + + return documentType + } + + var summary: String { + var parts: [String] = [] + + parts.append("Type: \(documentType)") + parts.append("Rev: \(revision)") + + let formatter = DateFormatter() + formatter.calendar = Calendar(identifier: .gregorian) + formatter.dateStyle = .short + parts.append("Created: \(formatter.string(from: createdAt))") + + return parts.joined(separator: " • ") + } + + init( + documentId: String, + documentType: String, + revision: Int32, + data: Data, + contractId: String, + ownerId: String, + network: Network + ) { + self.documentId = documentId + self.documentType = documentType + self.revision = revision + self.data = data + self.contractId = contractId + self.ownerId = ownerId + self.contractIdData = Data.identifier(fromBase58: contractId) ?? Data() + self.ownerIdData = Data.identifier(fromBase58: ownerId) ?? Data() + self.networkRaw = network.rawValue + self.createdAt = Date() + self.updatedAt = Date() + self.localCreatedAt = Date() + self.localUpdatedAt = Date() + } + + func updateProperties(_ newData: Data) { + self.data = newData + self.updatedAt = Date() + } + + func updateRevision(_ newRevision: Int64) { + self.revision = Int32(newRevision) + self.updatedAt = Date() + } + + func markAsDeleted() { + self.isDeleted = true + self.updatedAt = Date() + } + + static func predicate(documentId: String) -> Predicate { + #Predicate { doc in + doc.documentId == documentId && doc.isDeleted == false + } + } + + static func predicate(contractId: String, network: Network) -> Predicate { + let target = network.rawValue + return #Predicate { doc in + doc.contractId == contractId && doc.networkRaw == target && doc.isDeleted == false + } + } + + static func predicate(ownerId: Data) -> Predicate { + let ownerIdString = ownerId.toBase58String() + return #Predicate { doc in + doc.ownerId == ownerIdString && doc.isDeleted == false + } + } + + func linkToLocalIdentityIfNeeded(in modelContext: ModelContext) { + guard ownerIdentity == nil else { return } + + let ownerIdToMatch = self.ownerIdData + let identityPredicate = #Predicate { identity in + identity.identityId == ownerIdToMatch && identity.isLocal == true + } + + let descriptor = FetchDescriptor(predicate: identityPredicate) + + do { + if let localIdentity = try modelContext.fetch(descriptor).first { + self.ownerIdentity = localIdentity + self.localUpdatedAt = Date() + } + } catch { + print("Failed to link document to local identity: \(error)") + } + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocumentType.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocumentType.swift new file mode 100644 index 00000000000..06db414ae4b --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocumentType.swift @@ -0,0 +1,101 @@ +import Foundation +import SwiftData + +// `PersistentDocumentType` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentDocumentType { + @Attribute(.unique) var id: Data + var contractId: Data + var name: String + + var schemaJSON: Data + var propertiesJSON: Data + + var documentsKeepHistory: Bool + var documentsMutable: Bool + var documentsCanBeDeleted: Bool + var documentsTransferable: Bool + + var indexOnly: Bool = false + + var requiredFieldsJSON: Data? + + var securityLevel: Int + + var tradeMode: Int + var creationRestrictionMode: Int + + var requiresIdentityEncryptionBoundedKey: Bool + var requiresIdentityDecryptionBoundedKey: Bool + + var createdAt: Date + var lastAccessedAt: Date + + var dataContract: PersistentDataContract? + + @Relationship(deleteRule: .cascade, inverse: \PersistentDocument.documentType_relation) + var documents: [PersistentDocument]? + + @Relationship(deleteRule: .cascade, inverse: \PersistentIndex.documentType) + var indices: [PersistentIndex]? + + @Relationship(deleteRule: .cascade, inverse: \PersistentProperty.documentType) + var propertiesList: [PersistentProperty]? + + init(contractId: Data, name: String, schemaJSON: Data, propertiesJSON: Data) { + var idData = contractId + idData.append(name.data(using: .utf8) ?? Data()) + self.id = idData + + self.contractId = contractId + self.name = name + self.schemaJSON = schemaJSON + self.propertiesJSON = propertiesJSON + self.documentsKeepHistory = false + self.documentsMutable = true + self.documentsCanBeDeleted = true + self.documentsTransferable = false + self.securityLevel = 0 + self.tradeMode = 0 + self.creationRestrictionMode = 0 + self.requiresIdentityEncryptionBoundedKey = false + self.requiresIdentityDecryptionBoundedKey = false + self.createdAt = Date() + self.lastAccessedAt = Date() + } + } +} + +extension DashSchemaV1.PersistentDocumentType { + var contractIdBase58: String { + contractId.toBase58String() + } + + var schema: [String: Any]? { + try? JSONSerialization.jsonObject(with: schemaJSON, options: []) as? [String: Any] + } + + var properties: [String: Any]? { + try? JSONSerialization.jsonObject(with: propertiesJSON, options: []) as? [String: Any] + } + + var persistentProperties: [DashSchemaV1.PersistentProperty]? { + return propertiesList + } + + var requiredFields: [String]? { + guard let data = requiredFieldsJSON else { return nil } + return try? JSONSerialization.jsonObject(with: data, options: []) as? [String] + } + + var documentCount: Int { + documents?.count ?? 0 + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIdentity.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIdentity.swift new file mode 100644 index 00000000000..7f8df4e4e11 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIdentity.swift @@ -0,0 +1,274 @@ +import Foundation +import SwiftData + +// `PersistentIdentity` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentIdentity { + #Index([\.networkRaw]) + + @Attribute(.unique) var identityId: Data + var balance: Int64 + var revision: Int64 + var isLocal: Bool + var alias: String? + var dpnsName: String? + var mainDpnsName: String? + var identityType: String + + var votingPrivateKeyIdentifier: String? + var ownerPrivateKeyIdentifier: String? + var payoutPrivateKeyIdentifier: String? + + @Relationship(deleteRule: .cascade) var publicKeys: [PersistentPublicKey] + + var createdAt: Date + var lastUpdated: Date + var lastSyncedAt: Date? + + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + var wallet: PersistentWallet? + var identityIndex: UInt32 = 0 + + @Relationship(deleteRule: .cascade, inverse: \PersistentDocument.ownerIdentity) var documents: [PersistentDocument] + @Relationship(deleteRule: .nullify) var tokenBalances: [PersistentTokenBalance] + + @Relationship(deleteRule: .cascade, inverse: \PersistentDPNSName.identity) + var dpnsNames: [PersistentDPNSName] = [] + + @Relationship(deleteRule: .cascade, inverse: \PersistentDashpayProfile.identity) + var dashpayProfile: PersistentDashpayProfile? + + @Relationship(deleteRule: .cascade, inverse: \PersistentDashpayContactRequest.owner) + var contactRequests: [PersistentDashpayContactRequest] = [] + + @Relationship(deleteRule: .cascade, inverse: \PersistentDashpayPayment.owner) + var dashpayPayments: [PersistentDashpayPayment] = [] + + @Relationship(deleteRule: .cascade, inverse: \PersistentDashpayIgnoredSender.owner) + var dashpayIgnoredSenders: [PersistentDashpayIgnoredSender] = [] + + @Relationship(deleteRule: .cascade, inverse: \PersistentDashpayContactProfile.owner) + var contactProfiles: [PersistentDashpayContactProfile] = [] + + var ownedDataContracts: [PersistentDataContract] + + init( + identityId: Data, + balance: Int64 = 0, + revision: Int64 = 0, + isLocal: Bool = true, + alias: String? = nil, + dpnsName: String? = nil, + mainDpnsName: String? = nil, + identityType: IdentityType = .user, + votingPrivateKeyIdentifier: String? = nil, + ownerPrivateKeyIdentifier: String? = nil, + payoutPrivateKeyIdentifier: String? = nil, + network: Network, + identityIndex: UInt32 = 0 + ) { + self.identityId = identityId + self.balance = balance + self.revision = revision + self.isLocal = isLocal + self.alias = alias + self.dpnsName = dpnsName + self.mainDpnsName = mainDpnsName + self.identityType = identityType.rawValue + self.votingPrivateKeyIdentifier = votingPrivateKeyIdentifier + self.ownerPrivateKeyIdentifier = ownerPrivateKeyIdentifier + self.payoutPrivateKeyIdentifier = payoutPrivateKeyIdentifier + self.networkRaw = network.rawValue + self.identityIndex = identityIndex + self.publicKeys = [] + self.documents = [] + self.tokenBalances = [] + self.dpnsNames = [] + self.dashpayProfile = nil + self.contactRequests = [] + self.dashpayPayments = [] + self.dashpayIgnoredSenders = [] + self.contactProfiles = [] + self.ownedDataContracts = [] + self.createdAt = Date() + self.lastUpdated = Date() + self.lastSyncedAt = nil + } + + var identityIdString: String { + identityId.toHexString() + } + + var identityIdBase58: String { + identityId.toBase58String() + } + + var formattedBalance: String { + let dashAmount = Double(balance) / 100_000_000_000 + return String(format: "%.8f DASH", dashAmount) + } + + var identityPublicKeys: [IdentityPublicKey] { + publicKeys.compactMap { $0.toIdentityPublicKey() } + } + + var displayName: String { + if let alias = alias, !alias.isEmpty { + return alias + } + if let mainDpnsName = mainDpnsName, !mainDpnsName.isEmpty { + return mainDpnsName + } + if let dpnsName = dpnsName, !dpnsName.isEmpty { + return dpnsName + } + return String(identityIdString.prefix(12)) + "..." + } + + var identityTypeEnum: IdentityType { + IdentityType(rawValue: identityType) ?? .user + } + + func updateBalance(_ newBalance: Int64) { + self.balance = newBalance + self.lastUpdated = Date() + } + + func updateRevision(_ newRevision: Int64) { + self.revision = newRevision + self.lastUpdated = Date() + } + + func markAsSynced() { + self.lastSyncedAt = Date() + } + + func updateDPNSName(_ name: String?) { + self.dpnsName = name + self.lastUpdated = Date() + } + + func addPublicKey(_ key: PersistentPublicKey) { + publicKeys.append(key) + lastUpdated = Date() + } + + func removePublicKey(withId keyId: Int32) { + publicKeys.removeAll { $0.keyId == keyId } + lastUpdated = Date() + } + } +} + +extension DashSchemaV1.PersistentIdentity { + static func predicate(identityId: Data) -> Predicate { + #Predicate { identity in + identity.identityId == identityId + } + } + + static var walletOwnedIdentitiesPredicate: Predicate { + #Predicate { identity in + identity.wallet != nil + } + } + + static func predicate(type: IdentityType) -> Predicate { + let typeString = type.rawValue + return #Predicate { identity in + identity.identityType == typeString + } + } + + static func needsSyncPredicate(olderThan date: Date) -> Predicate { + #Predicate { identity in + identity.lastSyncedAt == nil || identity.lastSyncedAt! < date + } + } + + static func predicate(network: Network) -> Predicate { + let target = network.rawValue + return #Predicate { identity in + identity.networkRaw == target + } + } + + static func walletOwnedIdentitiesPredicate(network: Network) -> Predicate { + let target = network.rawValue + return #Predicate { identity in + identity.wallet != nil && identity.networkRaw == target + } + } + + static func fetch( + in context: ModelContext, + identityId: Data + ) -> DashSchemaV1.PersistentIdentity? { + let target = identityId + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.identityId == target } + ) + return try? context.fetch(descriptor).first + } +} + +extension DashSchemaV1.PersistentIdentity { + @discardableResult + static func updateBalance( + in context: ModelContext, + identityId: Data, + balance: UInt64 + ) -> Bool { + guard let row = fetch(in: context, identityId: identityId) else { return false } + row.balance = Int64(bitPattern: balance) + row.lastUpdated = Date() + return true + } + + @discardableResult + static func updateDpnsName( + in context: ModelContext, + identityId: Data, + dpnsName: String? + ) -> Bool { + guard let row = fetch(in: context, identityId: identityId) else { return false } + row.dpnsName = dpnsName + row.lastUpdated = Date() + return true + } + + @discardableResult + static func updateMainDpnsName( + in context: ModelContext, + identityId: Data, + mainDpnsName: String? + ) -> Bool { + guard let row = fetch(in: context, identityId: identityId) else { return false } + row.mainDpnsName = mainDpnsName + row.lastUpdated = Date() + return true + } + + @discardableResult + static func remove( + in context: ModelContext, + identityId: Data + ) -> Bool { + guard let row = fetch(in: context, identityId: identityId) else { return false } + context.delete(row) + return true + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIndex.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIndex.swift new file mode 100644 index 00000000000..a58f67a72c5 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIndex.swift @@ -0,0 +1,86 @@ +import Foundation +import SwiftData + +// `PersistentIndex` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentIndex { + @Attribute(.unique) var id: Data + var contractId: Data + var documentTypeName: String + var name: String + + var unique: Bool + var nullSearchable: Bool + var contested: Bool + + var countable: String? + var rangeCountable: Bool = false + var summable: String? + var rangeSummable: Bool = false + var averageable: String? + var rangeAverageable: Bool = false + + var rankedCountable: Bool = false + var rankedSummable: Bool = false + var rankedAverageable: Bool = false + + var terminal: String? + + var preallocated: Bool = false + + var timeRangeJSON: Data? + + var propertiesJSON: Data + + var contestedDetailsJSON: Data? + + var createdAt: Date + + var documentType: PersistentDocumentType? + + init(contractId: Data, documentTypeName: String, name: String, properties: [String]) { + var idData = contractId + idData.append(documentTypeName.data(using: .utf8) ?? Data()) + idData.append(name.data(using: .utf8) ?? Data()) + self.id = idData + + self.contractId = contractId + self.documentTypeName = documentTypeName + self.name = name + self.unique = false + self.nullSearchable = false + self.contested = false + + if let jsonData = try? JSONSerialization.data(withJSONObject: properties, options: []) { + self.propertiesJSON = jsonData + } else { + self.propertiesJSON = Data() + } + + self.createdAt = Date() + } + } +} + +extension DashSchemaV1.PersistentIndex { + var properties: [String]? { + try? JSONSerialization.jsonObject(with: propertiesJSON, options: []) as? [String] + } + + var contestedDetails: [String: Any]? { + guard let data = contestedDetailsJSON else { return nil } + return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } + + var timeRange: [String: Any]? { + guard let data = timeRangeJSON else { return nil } + return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentInvitation.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentInvitation.swift new file mode 100644 index 00000000000..1738a04d3f8 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentInvitation.swift @@ -0,0 +1,73 @@ +import Foundation +import SwiftData + +// `PersistentInvitation` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentInvitation { + #Index([\.walletId]) + + @Attribute(.unique) var outPointHex: String + + var rawOutPoint: Data + + var walletId: Data + + var fundingIndexRaw: Int + + var amountDuffs: Int64 + + var expiryUnix: Int + + var createdAtSecs: Int + + var hasInviter: Bool + + var statusRaw: Int + + var reclaimInFlight: Bool = false + + var createdAt: Date + var updatedAt: Date + + init( + outPointHex: String, + rawOutPoint: Data, + walletId: Data, + fundingIndexRaw: Int, + amountDuffs: Int64, + expiryUnix: Int, + createdAtSecs: Int, + hasInviter: Bool, + statusRaw: Int, + reclaimInFlight: Bool = false + ) { + self.outPointHex = outPointHex + self.rawOutPoint = rawOutPoint + self.walletId = walletId + self.fundingIndexRaw = fundingIndexRaw + self.amountDuffs = amountDuffs + self.expiryUnix = expiryUnix + self.createdAtSecs = createdAtSecs + self.hasInviter = hasInviter + self.statusRaw = statusRaw + self.reclaimInFlight = reclaimInFlight + self.createdAt = Date() + self.updatedAt = Date() + } + } +} + +extension DashSchemaV1.PersistentInvitation { + static func predicate(walletId: Data) -> Predicate { + #Predicate { entry in + entry.walletId == walletId + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentKeyword.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentKeyword.swift new file mode 100644 index 00000000000..d96d4244488 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentKeyword.swift @@ -0,0 +1,40 @@ +import Foundation +import SwiftData + +// `PersistentKeyword` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentKeyword { + @Attribute(.unique) var id: String + var keyword: String + var contractId: String + + var dataContract: PersistentDataContract? + + init(keyword: String, contractId: String) { + self.id = "\(contractId)_\(keyword)" + self.keyword = keyword + self.contractId = contractId + } + } +} + +extension DashSchemaV1.PersistentKeyword { + static func predicate(keyword: String) -> Predicate { + #Predicate { item in + item.keyword.localizedStandardContains(keyword) + } + } + + static func predicate(contractId: String) -> Predicate { + #Predicate { item in + item.contractId == contractId + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentMasternode.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentMasternode.swift new file mode 100644 index 00000000000..c1797e9b3fe --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentMasternode.swift @@ -0,0 +1,185 @@ +import Foundation +import SwiftData + +// `PersistentMasternode` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentMasternode { + #Unique([\.walletId, \.proTxHash]) + + var walletId: Data + var proTxHash: Data + var registrationTxid: Data + + var serviceAddress: String? + var isEvonode: Bool + + var ownerKeyHash: Data? + var votingKeyHash: Data? + var ownerAddress: String? + var votingAddress: String? + var operatorPublicKey: Data? + var platformNodeId: Data? + var payoutAddress: String? + var operatorPseudoAddress: String? + var platformNodeAddress: String? + + var ownerInWallet: Bool = false + var ownerAccountType: UInt8 = 0 + var ownerKeyIndex: UInt32 = 0 + var votingInWallet: Bool = false + var votingAccountType: UInt8 = 0 + var votingKeyIndex: UInt32 = 0 + var operatorInWallet: Bool = false + var operatorAccountType: UInt8 = 0 + var operatorKeyIndex: UInt32 = 0 + var platformInWallet: Bool = false + var platformAccountType: UInt8 = 0 + var platformKeyIndex: UInt32 = 0 + + var collateralTxid: Data? + var collateralVout: UInt32 + + var revoked: Bool + var revocationReason: UInt16 + var statusRaw: UInt8 = 3 + + var registrationHeight: UInt32 + var hasRegistration: Bool + var txCount: UInt32 + + var orderIndex: UInt32 + var typeIndex: UInt32 = 0 + + var createdAt: Date + var lastUpdated: Date + + init( + walletId: Data, + proTxHash: Data, + registrationTxid: Data, + serviceAddress: String? = nil, + isEvonode: Bool = false, + ownerKeyHash: Data? = nil, + votingKeyHash: Data? = nil, + ownerAddress: String? = nil, + votingAddress: String? = nil, + operatorPublicKey: Data? = nil, + platformNodeId: Data? = nil, + payoutAddress: String? = nil, + collateralTxid: Data? = nil, + collateralVout: UInt32 = 0, + revoked: Bool = false, + revocationReason: UInt16 = 0, + statusRaw: UInt8 = 3, + registrationHeight: UInt32 = 0, + hasRegistration: Bool = false, + txCount: UInt32 = 0, + orderIndex: UInt32 = 0, + typeIndex: UInt32 = 0 + ) { + self.walletId = walletId + self.proTxHash = proTxHash + self.registrationTxid = registrationTxid + self.serviceAddress = serviceAddress + self.isEvonode = isEvonode + self.ownerKeyHash = ownerKeyHash + self.votingKeyHash = votingKeyHash + self.ownerAddress = ownerAddress + self.votingAddress = votingAddress + self.operatorPublicKey = operatorPublicKey + self.platformNodeId = platformNodeId + self.payoutAddress = payoutAddress + self.collateralTxid = collateralTxid + self.collateralVout = collateralVout + self.revoked = revoked + self.revocationReason = revocationReason + self.statusRaw = statusRaw + self.registrationHeight = registrationHeight + self.hasRegistration = hasRegistration + self.txCount = txCount + self.orderIndex = orderIndex + self.typeIndex = typeIndex + self.createdAt = Date() + self.lastUpdated = Date() + } + + var proTxHashHex: String { + proTxHash.reversed().map { String(format: "%02x", $0) }.joined() + } + + var proTxHashShort: String { + let hex = proTxHashHex + guard hex.count >= 12 else { return hex } + return "\(String(hex.prefix(6)))…\(String(hex.suffix(6)))" + } + + var ownerKeyHashHex: String? { + ownerKeyHash.map { $0.map { String(format: "%02x", $0) }.joined() } + } + + var votingKeyHashHex: String? { + votingKeyHash.map { $0.map { String(format: "%02x", $0) }.joined() } + } + + static func providerAccountTypeName(_ tag: UInt8) -> String { + switch tag { + case 8: return "ProviderVotingKeys" + case 9: return "ProviderOwnerKeys" + case 10: return "ProviderOperatorKeys" + case 11: return "ProviderPlatformKeys" + default: return "Unknown(\(tag))" + } + } + + static func keyOwnershipLabel( + inWallet: Bool, + accountType: UInt8, + index: UInt32 + ) -> String { + inWallet + ? "\(providerAccountTypeName(accountType)) #\(index)" + : "not in this wallet" + } + + var operatorPublicKeyHex: String? { + operatorPublicKey.map { $0.map { String(format: "%02x", $0) }.joined() } + } + + var platformNodeIdHex: String? { + platformNodeId.map { $0.map { String(format: "%02x", $0) }.joined() } + } + + var collateralDisplay: String? { + guard let txid = collateralTxid else { return nil } + let hex = txid.reversed().map { String(format: "%02x", $0) }.joined() + return "\(hex):\(collateralVout)" + } + + var displayNumber: Int { + Int(typeIndex) + } + + var typeName: String { + isEvonode ? "Evonode" : "Masternode" + } + + var displayTitle: String { + "\(typeName) \(displayNumber)" + } + + var status: MasternodeStatus { + MasternodeStatus(rawValue: statusRaw) ?? .unknown + } + + var statusName: String { + status.displayName + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPendingInput.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPendingInput.swift new file mode 100644 index 00000000000..45322c7894f --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPendingInput.swift @@ -0,0 +1,42 @@ +import Foundation +import SwiftData + +// `PersistentPendingInput` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentPendingInput { + #Index([\.outpoint], [\.walletId]) + var outpoint: Data + + var inputIndex: UInt32 + + var spendingTxid: Data + + var spendingTransaction: PersistentTransaction? + + var walletId: Data + + var createdAt: Date + + init( + outpoint: Data, + inputIndex: UInt32, + spendingTxid: Data, + spendingTransaction: PersistentTransaction?, + walletId: Data + ) { + self.outpoint = outpoint + self.inputIndex = inputIndex + self.spendingTxid = spendingTxid + self.spendingTransaction = spendingTransaction + self.walletId = walletId + self.createdAt = Date() + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddress.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddress.swift new file mode 100644 index 00000000000..6a457fdd2e8 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddress.swift @@ -0,0 +1,78 @@ +import Foundation +import SwiftData + +// `PersistentPlatformAddress` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentPlatformAddress { + #Index([\.walletId]) + + @Attribute(.unique) var address: String + var addressType: UInt8 + @Attribute(.unique) var addressHash: Data + var publicKey: Data + var accountIndex: UInt32 + var addressIndex: UInt32 + var derivationPath: String + var isUsed: Bool + var balance: UInt64 + var nonce: UInt32 + var firstSeenHeight: UInt32 + var lastSeenHeight: UInt64 + var walletId: Data + var createdAt: Date + var lastUpdated: Date + + var account: PersistentAccount? + + init( + address: String, + addressType: UInt8, + addressHash: Data, + publicKey: Data = Data(), + accountIndex: UInt32, + addressIndex: UInt32, + derivationPath: String, + isUsed: Bool = false, + balance: UInt64 = 0, + nonce: UInt32 = 0, + walletId: Data + ) { + self.address = address + self.addressType = addressType + self.addressHash = addressHash + self.publicKey = publicKey + self.accountIndex = accountIndex + self.addressIndex = addressIndex + self.derivationPath = derivationPath + self.isUsed = isUsed + self.balance = balance + self.nonce = nonce + self.firstSeenHeight = 0 + self.lastSeenHeight = 0 + self.walletId = walletId + self.createdAt = Date() + self.lastUpdated = Date() + } + } +} + +extension DashSchemaV1.PersistentPlatformAddress { + static func predicate(walletId: Data) -> Predicate { + #Predicate { entry in + entry.walletId == walletId + } + } + + static var nonZeroBalancesPredicate: Predicate { + #Predicate { entry in + entry.balance > 0 + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddressesSyncState.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddressesSyncState.swift new file mode 100644 index 00000000000..350f0edc21c --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddressesSyncState.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftData + +// `PersistentPlatformAddressesSyncState` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentPlatformAddressesSyncState { + @Attribute(.unique) var walletId: Data + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + var syncHeight: UInt64 + var syncTimestamp: UInt64 + var lastKnownRecentBlock: UInt64 + var lastUpdated: Date + + init( + walletId: Data, + network: Network, + syncHeight: UInt64, + syncTimestamp: UInt64, + lastKnownRecentBlock: UInt64 + ) { + self.walletId = walletId + self.networkRaw = network.rawValue + self.syncHeight = syncHeight + self.syncTimestamp = syncTimestamp + self.lastKnownRecentBlock = lastKnownRecentBlock + self.lastUpdated = Date() + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentProperty.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentProperty.swift new file mode 100644 index 00000000000..9246487d673 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentProperty.swift @@ -0,0 +1,55 @@ +import Foundation +import SwiftData + +// `PersistentProperty` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentProperty { + @Attribute(.unique) var id: Data + var contractId: Data + var documentTypeName: String + var name: String + + var type: String + var format: String? + var contentMediaType: String? + var byteArray: Bool + var minItems: Int? + var maxItems: Int? + var pattern: String? + var minLength: Int? + var maxLength: Int? + var minValue: Int? + var maxValue: Int? + var fieldDescription: String? + + var transient: Bool + var isRequired: Bool + + var createdAt: Date + + var documentType: PersistentDocumentType? + + init(contractId: Data, documentTypeName: String, name: String, type: String) { + var idData = contractId + idData.append(documentTypeName.data(using: .utf8) ?? Data()) + idData.append(name.data(using: .utf8) ?? Data()) + self.id = idData + + self.contractId = contractId + self.documentTypeName = documentTypeName + self.name = name + self.type = type + self.byteArray = false + self.transient = false + self.isRequired = false + self.createdAt = Date() + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPublicKey.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPublicKey.swift new file mode 100644 index 00000000000..e9bbce743dd --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPublicKey.swift @@ -0,0 +1,171 @@ +import Foundation +import SwiftData + +// `PersistentPublicKey` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentPublicKey { + var keyId: Int32 + var purpose: String + var securityLevel: String + var keyType: String + var readOnly: Bool + var disabledAt: Int64? + + var publicKeyData: Data + + var contractBoundsData: Data? + + var contractBoundsDocumentTypeName: String? + + var privateKeyKeychainIdentifier: String? + + var walletId: Data? + + var identityDerivationPath: String? + + var identityId: String + var createdAt: Date + var lastAccessed: Date? + + @Relationship(inverse: \PersistentIdentity.publicKeys) + var identity: PersistentIdentity? + + init( + keyId: Int32, + purpose: KeyPurpose, + securityLevel: SecurityLevel, + keyType: KeyType, + publicKeyData: Data, + readOnly: Bool = false, + disabledAt: Int64? = nil, + contractBounds: [Data]? = nil, + contractBoundsDocumentTypeName: String? = nil, + identityId: String + ) { + self.keyId = keyId + self.purpose = String(purpose.rawValue) + self.securityLevel = String(securityLevel.rawValue) + self.keyType = String(keyType.rawValue) + self.publicKeyData = publicKeyData + self.readOnly = readOnly + self.disabledAt = disabledAt + if let contractBounds = contractBounds { + self.contractBoundsData = try? JSONSerialization.data(withJSONObject: contractBounds.map { $0.base64EncodedString() }) + } else { + self.contractBoundsData = nil + } + self.contractBoundsDocumentTypeName = contractBoundsDocumentTypeName + self.identityId = identityId + self.createdAt = Date() + } + + var contractBounds: [Data]? { + get { + guard let data = contractBoundsData, + let json = try? JSONSerialization.jsonObject(with: data), + let strings = json as? [String] else { + return nil + } + return strings.compactMap { Data(base64Encoded: $0) } + } + set { + contractBoundsDocumentTypeName = nil + if let newValue = newValue { + contractBoundsData = try? JSONSerialization.data(withJSONObject: newValue.map { $0.base64EncodedString() }) + } else { + contractBoundsData = nil + } + } + } + + var purposeEnum: KeyPurpose? { + guard let purposeInt = UInt8(purpose) else { return nil } + return KeyPurpose(rawValue: purposeInt) + } + + var securityLevelEnum: SecurityLevel? { + guard let levelInt = UInt8(securityLevel) else { return nil } + return SecurityLevel(rawValue: levelInt) + } + + var keyTypeEnum: KeyType? { + guard let typeInt = UInt8(keyType) else { return nil } + return KeyType(rawValue: typeInt) + } + + var isDisabled: Bool { + disabledAt != nil + } + + var hasPrivateKeyIdentifier: Bool { + privateKeyKeychainIdentifier != nil + } + } +} + +extension DashSchemaV1.PersistentPublicKey { + func toIdentityPublicKey() -> IdentityPublicKey? { + guard let purpose = purposeEnum, + let securityLevel = securityLevelEnum, + let keyType = keyTypeEnum else { + return nil + } + + let bounds: ContractBounds? + if let id = contractBounds?.first, id.count == 32 { + if let docTypeName = contractBoundsDocumentTypeName, !docTypeName.isEmpty { + bounds = .singleContractDocumentType(id: id, documentTypeName: docTypeName) + } else { + bounds = .singleContract(id: id) + } + } else { + bounds = nil + } + + return IdentityPublicKey( + id: KeyID(keyId), + purpose: purpose, + securityLevel: securityLevel, + contractBounds: bounds, + keyType: keyType, + readOnly: readOnly, + data: publicKeyData, + disabledAt: disabledAt.map { TimestampMillis($0) } + ) + } + + static func from(_ publicKey: IdentityPublicKey, identityId: String) -> DashSchemaV1.PersistentPublicKey? { + let boundsIds: [Data]? + let docTypeName: String? + switch publicKey.contractBounds { + case .singleContract(let id): + boundsIds = [id] + docTypeName = nil + case .singleContractDocumentType(let id, let name): + boundsIds = [id] + docTypeName = name + case .none: + boundsIds = nil + docTypeName = nil + } + return DashSchemaV1.PersistentPublicKey( + keyId: Int32(publicKey.id), + purpose: publicKey.purpose, + securityLevel: publicKey.securityLevel, + keyType: publicKey.keyType, + publicKeyData: publicKey.data, + readOnly: publicKey.readOnly, + disabledAt: publicKey.disabledAt.map { Int64($0) }, + contractBounds: boundsIds, + contractBoundsDocumentTypeName: docTypeName, + identityId: identityId + ) + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedActivity.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedActivity.swift new file mode 100644 index 00000000000..177340b2798 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedActivity.swift @@ -0,0 +1,89 @@ +import Foundation +import SwiftData + +// `PersistentShieldedActivity` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentShieldedActivity { + #Unique([\.walletId, \.accountIndex, \.entryId]) + #Index([\.walletId, \.accountIndex]) + + var walletId: Data + var accountIndex: UInt32 + var entryId: Data + + var kindTag: Int + var direction: Int + var status: Int + + var amount: UInt64 + var fee: UInt64 + var hasFee: Bool + var blockHeight: UInt64 + var hasBlockHeight: Bool + var createdAtMs: UInt64 + + var minNotePosition: UInt64 = 0 + var hasMinNotePosition: Bool = false + + var identityId: Data + var counterparty: Data + var memo: Data + var noteCmxs: Data + var spentNullifiers: Data + + var createdAt: Date + var lastUpdated: Date + + init( + walletId: Data, + accountIndex: UInt32, + entryId: Data, + kindTag: Int, + direction: Int, + status: Int, + amount: UInt64, + fee: UInt64, + hasFee: Bool, + blockHeight: UInt64, + hasBlockHeight: Bool, + createdAtMs: UInt64, + minNotePosition: UInt64 = 0, + hasMinNotePosition: Bool = false, + identityId: Data, + counterparty: Data, + memo: Data, + noteCmxs: Data, + spentNullifiers: Data + ) { + self.walletId = walletId + self.accountIndex = accountIndex + self.entryId = entryId + self.kindTag = kindTag + self.direction = direction + self.status = status + self.amount = amount + self.fee = fee + self.hasFee = hasFee + self.blockHeight = blockHeight + self.hasBlockHeight = hasBlockHeight + self.createdAtMs = createdAtMs + self.minNotePosition = minNotePosition + self.hasMinNotePosition = hasMinNotePosition + self.identityId = identityId + self.counterparty = counterparty + self.memo = memo + self.noteCmxs = noteCmxs + self.spentNullifiers = spentNullifiers + let now = Date() + self.createdAt = now + self.lastUpdated = now + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedNote.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedNote.swift new file mode 100644 index 00000000000..19615909f5b --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedNote.swift @@ -0,0 +1,66 @@ +import Foundation +import SwiftData + +// `PersistentShieldedNote` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentShieldedNote { + #Index([\.walletId, \.accountIndex]) + + var walletId: Data + var accountIndex: UInt32 + var position: UInt64 + var cmx: Data + @Attribute(.unique) var nullifier: Data + var blockHeight: UInt64 + var isSpent: Bool + var value: UInt64 + var noteData: Data + + var createdAt: Date + var lastUpdated: Date + + init( + walletId: Data, + accountIndex: UInt32, + position: UInt64, + cmx: Data, + nullifier: Data, + blockHeight: UInt64, + isSpent: Bool, + value: UInt64, + noteData: Data + ) { + self.walletId = walletId + self.accountIndex = accountIndex + self.position = position + self.cmx = cmx + self.nullifier = nullifier + self.blockHeight = blockHeight + self.isSpent = isSpent + self.value = value + self.noteData = noteData + let now = Date() + self.createdAt = now + self.lastUpdated = now + } + } +} + +extension DashSchemaV1.PersistentShieldedNote { + static func unspentPredicate(walletId: Data) -> Predicate { + #Predicate { + $0.walletId == walletId && $0.isSpent == false + } + } + + static var unspentPredicate: Predicate { + #Predicate { $0.isSpent == false } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedOutgoingNote.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedOutgoingNote.swift new file mode 100644 index 00000000000..f97b1bafbea --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedOutgoingNote.swift @@ -0,0 +1,49 @@ +import Foundation +import SwiftData + +// `PersistentShieldedOutgoingNote` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentShieldedOutgoingNote { + #Unique([\.walletId, \.accountIndex, \.cmx]) + #Index([\.walletId, \.accountIndex]) + + var walletId: Data + var accountIndex: UInt32 + var cmx: Data + var recipient: Data + var value: UInt64 + var memo: Data + var blockHeight: UInt64 + + var createdAt: Date + var lastUpdated: Date + + init( + walletId: Data, + accountIndex: UInt32, + cmx: Data, + recipient: Data, + value: UInt64, + memo: Data, + blockHeight: UInt64 + ) { + self.walletId = walletId + self.accountIndex = accountIndex + self.cmx = cmx + self.recipient = recipient + self.value = value + self.memo = memo + self.blockHeight = blockHeight + let now = Date() + self.createdAt = now + self.lastUpdated = now + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedSyncState.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedSyncState.swift new file mode 100644 index 00000000000..d944680abea --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedSyncState.swift @@ -0,0 +1,34 @@ +import Foundation +import SwiftData + +// `PersistentShieldedSyncState` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentShieldedSyncState { + #Unique([\.walletId, \.accountIndex]) + #Index([\.walletId]) + + var walletId: Data + var accountIndex: UInt32 + var lastSyncedIndex: UInt64 + + var lastUpdated: Date + + init( + walletId: Data, + accountIndex: UInt32, + lastSyncedIndex: UInt64 = 0 + ) { + self.walletId = walletId + self.accountIndex = accountIndex + self.lastSyncedIndex = lastSyncedIndex + self.lastUpdated = Date() + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedViewingKey.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedViewingKey.swift new file mode 100644 index 00000000000..ab2058653fd --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedViewingKey.swift @@ -0,0 +1,34 @@ +import Foundation +import SwiftData + +// `PersistentShieldedViewingKey` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentShieldedViewingKey { + #Unique([\.walletId, \.accountIndex]) + #Index([\.walletId]) + + var walletId: Data + var accountIndex: UInt32 + var fvkBytes: Data + + var lastUpdated: Date + + init( + walletId: Data, + accountIndex: UInt32, + fvkBytes: Data + ) { + self.walletId = walletId + self.accountIndex = accountIndex + self.fvkBytes = fvkBytes + self.lastUpdated = Date() + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentToken.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentToken.swift new file mode 100644 index 00000000000..2bef35d8fb8 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentToken.swift @@ -0,0 +1,376 @@ +import Foundation +import SwiftData + +// `PersistentToken` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentToken { + @Attribute(.unique) var id: Data + var contractId: Data + var position: Int + var name: String + + var baseSupply: String + var maxSupply: String? + var decimals: Int + + var localizations: [String: TokenLocalization]? + + var isPaused: Bool + var allowTransferToFrozenBalance: Bool + + var keepsTransferHistory: Bool + var keepsFreezingHistory: Bool + var keepsMintingHistory: Bool + var keepsBurningHistory: Bool + var keepsDirectPricingHistory: Bool + var keepsDirectPurchaseHistory: Bool + + var conventionsChangeRules: ChangeControlRules? + var maxSupplyChangeRules: ChangeControlRules? + var manualMintingRules: ChangeControlRules? + var manualBurningRules: ChangeControlRules? + var freezeRules: ChangeControlRules? + var unfreezeRules: ChangeControlRules? + var destroyFrozenFundsRules: ChangeControlRules? + var emergencyActionRules: ChangeControlRules? + + var perpetualDistribution: TokenPerpetualDistribution? + var preProgrammedDistribution: TokenPreProgrammedDistribution? + var newTokensDestinationIdentity: Data? + var mintingAllowChoosingDestination: Bool + var distributionChangeRules: TokenDistributionChangeRules? + + var tradeMode: TokenTradeMode + var tradeModeChangeRules: ChangeControlRules? + + var mainControlGroupPosition: Int? + var mainControlGroupCanBeModified: String? + + var tokenDescription: String? + + var createdAt: Date + var lastUpdatedAt: Date + + var dataContract: PersistentDataContract? + + @Relationship(deleteRule: .cascade) + var balances: [PersistentTokenBalance]? + + @Relationship(deleteRule: .cascade) + var historyEvents: [PersistentTokenHistoryEvent]? + + init(contractId: Data, position: Int, name: String, baseSupply: String, decimals: Int = 8) { + var idData = contractId + withUnsafeBytes(of: position.bigEndian) { bytes in + idData.append(contentsOf: bytes) + } + self.id = idData + + self.contractId = contractId + self.position = position + self.name = name + self.baseSupply = baseSupply + self.decimals = decimals + + self.isPaused = false + self.allowTransferToFrozenBalance = true + self.keepsTransferHistory = true + self.keepsFreezingHistory = true + self.keepsMintingHistory = true + self.keepsBurningHistory = true + self.keepsDirectPricingHistory = true + self.keepsDirectPurchaseHistory = true + self.mintingAllowChoosingDestination = true + self.tradeMode = TokenTradeMode.notTradeable + + self.createdAt = Date() + self.lastUpdatedAt = Date() + } + } +} + +extension DashSchemaV1.PersistentToken { + var displayName: String { + if let desc = tokenDescription, !desc.isEmpty { + return desc + } + return getSingularForm() ?? name + } + + var formattedBaseSupply: String { + Self.formatSupply(baseSupply, decimals: decimals) + } + + static func formatSupply(_ raw: String, decimals: Int) -> String { + guard !raw.isEmpty, raw.allSatisfy({ $0.isASCII && $0.isNumber }) else { + return raw + } + let normalized = String(raw.drop(while: { $0 == "0" })) + let digits = normalized.isEmpty ? "0" : normalized + let scale = max(0, decimals) + let integer: String + var fraction = "" + if scale == 0 { + integer = digits + } else if digits.count <= scale { + integer = "0" + fraction = String(repeating: "0", count: scale - digits.count) + digits + } else { + let split = digits.index(digits.endIndex, offsetBy: -scale) + integer = String(digits[.. 0 && offset.isMultiple(of: 3) { grouped.append(",") } + grouped.append(character) + } + grouped = String(grouped.reversed()) + return fraction.isEmpty ? grouped : "\(grouped).\(fraction)" + } + + var contractIdBase58: String { + contractId.toBase58String() + } + + var canManuallyMint: Bool { + manualMintingRules != nil + } + + var canManuallyBurn: Bool { + manualBurningRules != nil + } + + var canFreeze: Bool { + freezeRules != nil + } + + var canUnfreeze: Bool { + unfreezeRules != nil + } + + var canDestroyFrozenFunds: Bool { + destroyFrozenFundsRules != nil + } + + var hasEmergencyActions: Bool { + emergencyActionRules != nil + } + + var canChangeMaxSupply: Bool { + maxSupplyChangeRules != nil + } + + var canChangeConventions: Bool { + conventionsChangeRules != nil + } + + var hasDistribution: Bool { + perpetualDistribution != nil || preProgrammedDistribution != nil + } + + var canChangeTradeMode: Bool { + tradeModeChangeRules != nil + } + + var keepsAnyHistory: Bool { + keepsTransferHistory || + keepsFreezingHistory || + keepsMintingHistory || + keepsBurningHistory || + keepsDirectPricingHistory || + keepsDirectPurchaseHistory + } + + var totalSupply: String { + guard let balances = balances, !balances.isEmpty else { return baseSupply } + return Self.sumUnsignedBalances(balances.map(\.unsignedBalance)) + } + + var totalFrozenBalance: String { + guard let balances = balances else { return "0" } + return Self.sumUnsignedBalances( + balances.lazy.filter(\.frozen).map(\.unsignedBalance) + ) + } + + var activeHolders: Int { + balances?.filter { $0.unsignedBalance > 0 }.count ?? 0 + } + + private static func sumUnsignedBalances(_ values: S) -> String + where S.Element == UInt64 { + var digits: [UInt8] = [0] // little-endian decimal digits + + for value in values { + var carry = 0 + let addend = String(value).utf8.reversed().map { Int($0 - 48) } + let width = max(digits.count, addend.count) + if digits.count < width { + digits.append(contentsOf: repeatElement(0, count: width - digits.count)) + } + + for index in 0.. 0 { + digits.append(UInt8(carry % 10)) + carry /= 10 + } + } + + return String(digits.reversed().map { Character(String($0)) }) + } + + var hasMaxSupply: Bool { + maxSupply != nil + } + + var isTradeable: Bool { + tradeMode != .notTradeable + } + + var newTokensDestinationIdentityBase58: String? { + newTokensDestinationIdentity?.toBase58String() + } +} + +extension DashSchemaV1.PersistentToken { + func setLocalization(languageCode: String, singularForm: String, pluralForm: String, description: String? = nil) { + if localizations == nil { + localizations = [:] + } + localizations?[languageCode] = DashSchemaV1.TokenLocalization( + singularForm: singularForm, + pluralForm: pluralForm, + description: description + ) + lastUpdatedAt = Date() + } + + func getSingularForm(languageCode: String = "en") -> String? { + return localizations?[languageCode]?.singularForm ?? localizations?["en"]?.singularForm + } + + func getPluralForm(languageCode: String = "en") -> String? { + return localizations?[languageCode]?.pluralForm ?? localizations?["en"]?.pluralForm + } +} + +extension DashSchemaV1.PersistentToken { + func getChangeControlRules(for type: ChangeControlRuleType) -> DashSchemaV1.ChangeControlRules? { + switch type { + case .conventions: return conventionsChangeRules + case .maxSupply: return maxSupplyChangeRules + case .manualMinting: return manualMintingRules + case .manualBurning: return manualBurningRules + case .freeze: return freezeRules + case .unfreeze: return unfreezeRules + case .destroyFrozenFunds: return destroyFrozenFundsRules + case .emergencyAction: return emergencyActionRules + case .tradeMode: return tradeModeChangeRules + } + } + + func setChangeControlRules(_ rules: DashSchemaV1.ChangeControlRules, for type: ChangeControlRuleType) { + switch type { + case .conventions: conventionsChangeRules = rules + case .maxSupply: maxSupplyChangeRules = rules + case .manualMinting: manualMintingRules = rules + case .manualBurning: manualBurningRules = rules + case .freeze: freezeRules = rules + case .unfreeze: unfreezeRules = rules + case .destroyFrozenFunds: destroyFrozenFundsRules = rules + case .emergencyAction: emergencyActionRules = rules + case .tradeMode: tradeModeChangeRules = rules + } + + lastUpdatedAt = Date() + } +} + +extension DashSchemaV1.PersistentToken { + static func mintableTokensPredicate() -> Predicate { + #Predicate { token in + token.manualMintingRules != nil + } + } + + static func burnableTokensPredicate() -> Predicate { + #Predicate { token in + token.manualBurningRules != nil + } + } + + static func freezableTokensPredicate() -> Predicate { + #Predicate { token in + token.freezeRules != nil + } + } + + static func distributionTokensPredicate() -> Predicate { + #Predicate { token in + token.perpetualDistribution != nil || token.preProgrammedDistribution != nil + } + } + + static func pausedTokensPredicate() -> Predicate { + #Predicate { token in + token.isPaused == true + } + } + + static func tokensByContractPredicate(contractId: Data) -> Predicate { + #Predicate { token in + token.contractId == contractId + } + } + + static func tokensWithControlRulePredicate(rule: ControlRuleType) -> Predicate { + switch rule { + case .manualMinting: + return #Predicate { token in + token.manualMintingRules != nil + } + case .manualBurning: + return #Predicate { token in + token.manualBurningRules != nil + } + case .freeze: + return #Predicate { token in + token.freezeRules != nil + } + case .unfreeze: + return #Predicate { token in + token.unfreezeRules != nil + } + case .destroyFrozenFunds: + return #Predicate { token in + token.destroyFrozenFundsRules != nil + } + case .emergencyAction: + return #Predicate { token in + token.emergencyActionRules != nil + } + case .conventions: + return #Predicate { token in + token.conventionsChangeRules != nil + } + case .maxSupply: + return #Predicate { token in + token.maxSupplyChangeRules != nil + } + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenBalance.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenBalance.swift new file mode 100644 index 00000000000..097e968cd64 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenBalance.swift @@ -0,0 +1,198 @@ +import Foundation +import SwiftData + +// `PersistentTokenBalance` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentTokenBalance { + #Index([\.networkRaw]) + + var tokenId: String + var identityId: Data + var balance: Int64 + var frozen: Bool + + var createdAt: Date + var lastUpdated: Date + var lastSyncedAt: Date? + + var tokenName: String? + var tokenSymbol: String? + var tokenDecimals: Int32? + + var networkRaw: UInt32 + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + @Relationship(deleteRule: .nullify) var identity: PersistentIdentity? + @Relationship(inverse: \PersistentToken.balances) var token: PersistentToken? + + init( + tokenId: String, + identityId: Data, + balance: Int64 = 0, + frozen: Bool = false, + tokenName: String? = nil, + tokenSymbol: String? = nil, + tokenDecimals: Int32? = nil, + network: Network + ) { + self.tokenId = tokenId + self.identityId = identityId + self.balance = balance + self.frozen = frozen + self.tokenName = tokenName + self.tokenSymbol = tokenSymbol + self.tokenDecimals = tokenDecimals + self.createdAt = Date() + self.lastUpdated = Date() + self.lastSyncedAt = nil + self.networkRaw = network.rawValue + } + + convenience init( + tokenId: String, + identityId: Data, + unsignedBalance: UInt64, + frozen: Bool = false, + tokenName: String? = nil, + tokenSymbol: String? = nil, + tokenDecimals: Int32? = nil, + network: Network + ) { + self.init( + tokenId: tokenId, + identityId: identityId, + balance: Int64(bitPattern: unsignedBalance), + frozen: frozen, + tokenName: tokenName, + tokenSymbol: tokenSymbol, + tokenDecimals: tokenDecimals, + network: network + ) + } + + var unsignedBalance: UInt64 { + get { UInt64(bitPattern: balance) } + set { balance = Int64(bitPattern: newValue) } + } + + var formattedBalance: String { + let decimals: Int + if let tokenDecimals { + decimals = Int(tokenDecimals) + } else if let tokenDecimals = token?.decimals { + decimals = tokenDecimals + } else { + return "\(unsignedBalance)" + } + + guard decimals > 0 else { return String(unsignedBalance) } + + let digits = String(unsignedBalance) + let scale = decimals + if digits.count <= scale { + return "0." + String(repeating: "0", count: scale - digits.count) + digits + } + let split = digits.index(digits.endIndex, offsetBy: -scale) + return String(digits[.. (tokenId: String, balance: UInt64, frozen: Bool) { + return (tokenId: tokenId, balance: unsignedBalance, frozen: frozen) + } +} + +extension DashSchemaV1.PersistentTokenBalance { + static func predicate(tokenId: String, identityId: Data) -> Predicate { + #Predicate { balance in + balance.tokenId == tokenId && balance.identityId == identityId + } + } + + static func predicate(identityId: Data) -> Predicate { + #Predicate { balance in + balance.identityId == identityId + } + } + + static func predicate(tokenId: String) -> Predicate { + #Predicate { balance in + balance.tokenId == tokenId + } + } + + static var nonZeroBalancesPredicate: Predicate { + #Predicate { balance in + balance.balance != 0 + } + } + + static var frozenBalancesPredicate: Predicate { + #Predicate { balance in + balance.frozen == true + } + } + + static func needsSyncPredicate(olderThan date: Date) -> Predicate { + #Predicate { balance in + balance.lastSyncedAt == nil || balance.lastSyncedAt! < date + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenHistoryEvent.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenHistoryEvent.swift new file mode 100644 index 00000000000..f32e27eb254 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenHistoryEvent.swift @@ -0,0 +1,112 @@ +import Foundation +import SwiftData + +// `PersistentTokenHistoryEvent` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentTokenHistoryEvent { + @Attribute(.unique) var id: UUID + + var eventType: String + var transactionId: Data? + var blockHeight: Int64? + var coreBlockHeight: Int64? + + var fromIdentity: Data? + var toIdentity: Data? + var performedByIdentity: Data + + var amount: String? + var balanceBefore: String? + var balanceAfter: String? + + var additionalDataJSON: Data? + + var eventDescription: String? + + var createdAt: Date + var eventTimestamp: Date + + @Relationship(inverse: \PersistentToken.historyEvents) + var token: PersistentToken? + + init( + eventType: TokenEventType, + performedByIdentity: Data, + eventTimestamp: Date = Date() + ) { + self.id = UUID() + self.eventType = eventType.rawValue + self.performedByIdentity = performedByIdentity + self.eventTimestamp = eventTimestamp + self.createdAt = Date() + } + + var eventTypeEnum: TokenEventType { + TokenEventType(rawValue: eventType) ?? .unknown + } + + var fromIdentityBase58: String? { + fromIdentity?.toBase58String() + } + + var toIdentityBase58: String? { + toIdentity?.toBase58String() + } + + var performedByIdentityBase58: String { + performedByIdentity.toBase58String() + } + + var displayTitle: String { + switch eventTypeEnum { + case .mint: + return "Minted \(formattedAmount)" + case .burn: + return "Burned \(formattedAmount)" + case .transfer: + return "Transfer \(formattedAmount)" + case .freeze: + return "Frozen \(formattedAmount)" + case .unfreeze: + return "Unfrozen \(formattedAmount)" + case .destroyFrozenFunds: + return "Destroyed Frozen Funds \(formattedAmount)" + case .configUpdate: + return "Configuration Updated" + case .emergencyAction: + return "Emergency Action" + case .perpetualDistribution: + return "Perpetual Distribution \(formattedAmount)" + case .preProgrammedRelease: + return "Pre-programmed Release \(formattedAmount)" + case .directPricing: + return "Direct Pricing Updated" + case .directPurchase: + return "Direct Purchase \(formattedAmount)" + case .unknown: + return "Unknown Event" + } + } + + private var formattedAmount: String { + guard let amount = amount else { return "" } + return amount + } + + func setAdditionalData(_ data: [String: Any]) { + additionalDataJSON = try? JSONSerialization.data(withJSONObject: data) + } + + func getAdditionalData() -> [String: Any]? { + guard let data = additionalDataJSON else { return nil } + return try? JSONSerialization.jsonObject(with: data) as? [String: Any] + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTransaction.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTransaction.swift new file mode 100644 index 00000000000..d3a25a6b9c3 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTransaction.swift @@ -0,0 +1,167 @@ +import Foundation +import SwiftData + +// `PersistentTransaction` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentTransaction { + #Index([\.firstSeen]) + + @Attribute(.unique) var txid: Data + var transactionData: Data + var context: UInt32 + var blockHeight: UInt32 + var blockHash: Data? + var blockTimestamp: UInt32 + var blockPosition: UInt32 = 0 + var hasBlockPosition: Bool = false + var direction: UInt32 + var transactionType: String + var transactionTypeKind: UInt8 = 0xFF + var netAmount: Int64 + var fee: UInt64? + var label: String + var firstSeen: UInt64 + + var providerServiceAddress: String? = nil + var providerProTxHash: Data? = nil + var providerCollateralTxid: Data? = nil + var providerCollateralVout: UInt32 = 0 + var providerOwnerKeyHash: Data? = nil + var providerVotingKeyHash: Data? = nil + + var createdAt: Date + var lastUpdated: Date + + @Relationship(deleteRule: .cascade, inverse: \PersistentTxo.transaction) + var outputs: [PersistentTxo] = [] + + @Relationship(inverse: \PersistentTxo.spendingTransaction) + var inputs: [PersistentTxo] = [] + + @Relationship(deleteRule: .cascade, inverse: \PersistentPendingInput.spendingTransaction) + var pendingInputs: [PersistentPendingInput] = [] + + @Relationship(inverse: \PersistentAccount.involvedTransactions) + var involvedAccounts: [PersistentAccount] = [] + + init( + txid: Data, + transactionData: Data, + context: UInt32 = 0, + blockHeight: UInt32 = 0, + direction: UInt32 = 0, + transactionType: String = "Standard", + netAmount: Int64 = 0, + firstSeen: UInt64 = 0 + ) { + self.txid = txid + self.transactionData = transactionData + self.context = context + self.blockHeight = blockHeight + self.blockTimestamp = 0 + self.direction = direction + self.transactionType = transactionType + self.netAmount = netAmount + self.firstSeen = firstSeen + self.label = "" + self.createdAt = Date() + self.lastUpdated = Date() + } + + var txidHex: String { + txid.reversed().map { String(format: "%02x", $0) }.joined() + } + + var contextName: String { + switch context { + case 0: return "Mempool" + case 1: return "InstantSend" + case 2: return "In Block" + case 3: return "Chain Locked" + default: return "Unknown" + } + } + + var directionName: String { + switch direction { + case 0: return "Incoming" + case 1: return "Outgoing" + case 2: return "Internal" + case 3: return "CoinJoin" + default: return "Unknown" + } + } + + var typedKind: TransactionTypeKind? { + TransactionTypeKind(rawValue: transactionTypeKind) + } + + var isAssetLock: Bool { + typedKind == .assetLock + } + + var isAssetUnlock: Bool { + typedKind == .assetUnlock + } + + var isProviderRegistration: Bool { + typedKind == .providerRegistration + } + + var isProviderUpdateService: Bool { + typedKind == .providerUpdateService + } + + var providerProTxHashHex: String? { + providerProTxHash.map { $0.reversed().map { String(format: "%02x", $0) }.joined() } + } + + var providerCollateralDisplay: String? { + guard let txid = providerCollateralTxid else { return nil } + let hex = txid.reversed().map { String(format: "%02x", $0) }.joined() + return "\(hex):\(providerCollateralVout)" + } + + var providerOwnerKeyHashHex: String? { + providerOwnerKeyHash.map { $0.map { String(format: "%02x", $0) }.joined() } + } + + var providerVotingKeyHashHex: String? { + providerVotingKeyHash.map { $0.map { String(format: "%02x", $0) }.joined() } + } + + var isProviderSpecial: Bool { + providerSpecialName != nil + } + + var providerSpecialName: String? { + switch typedKind { + case .providerRegistration: return "Provider Registration" + case .providerUpdateRegistrar: return "Provider Update Registrar" + case .providerUpdateService: return "Provider Update Service" + case .providerUpdateRevocation: return "Provider Update Revocation" + default: return nil + } + } + + var displayDirection: String { + if isAssetLock { return "Asset Lock" } + if isAssetUnlock { return "Asset Unlock" } + if let name = providerSpecialName { return name } + return directionName + } + + var formattedAmount: String { + let dash = Double(abs(netAmount)) / 100_000_000.0 + let sign = netAmount >= 0 ? "+" : "-" + return String(format: "%@%.8f DASH", sign, dash) + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTxo.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTxo.swift new file mode 100644 index 00000000000..86095488bd6 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTxo.swift @@ -0,0 +1,97 @@ +import Foundation +import SwiftData + +// `PersistentTxo` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentTxo { + #Index([\.walletId]) + + @Attribute(.unique) var outpoint: Data + var vout: UInt32 + var amount: UInt64 + var address: String + var scriptPubKey: Data + var height: UInt32 + var isCoinbase: Bool + var isConfirmed: Bool + var isInstantLocked: Bool + var isLocked: Bool + var isSpent: Bool + var createdAt: Date + var lastUpdated: Date + + var walletId: Data = Data() + + var transaction: PersistentTransaction? + + var spendingTransaction: PersistentTransaction? + + var spendingInputIndex: UInt32? = nil + + var account: PersistentAccount? + + var coreAddress: PersistentCoreAddress? + + init( + transaction: PersistentTransaction, + vout: UInt32, + amount: UInt64, + address: String, + scriptPubKey: Data = Data(), + height: UInt32 = 0 + ) { + self.outpoint = Self.makeOutpoint(txid: transaction.txid, vout: vout) + self.vout = vout + self.amount = amount + self.address = address + self.scriptPubKey = scriptPubKey + self.height = height + self.isCoinbase = false + self.isConfirmed = false + self.isInstantLocked = false + self.isLocked = false + self.isSpent = false + self.createdAt = Date() + self.lastUpdated = Date() + self.transaction = transaction + } + + static func makeOutpoint(txid: Data, vout: UInt32) -> Data { + var data = Data(capacity: 36) + data.append(txid) + var v = vout.littleEndian + withUnsafeBytes(of: &v) { data.append(contentsOf: $0) } + return data + } + + var txid: Data { + if let transaction { + return transaction.txid + } + return outpoint.count >= 32 ? Data(outpoint.prefix(32)) : Data() + } + + var txidHex: String { + let rawTxid = txid + guard rawTxid.count == 32 else { return "" } + return rawTxid.reversed().map { String(format: "%02x", $0) }.joined() + } + + var outpointHex: String { + let hex = txidHex + return hex.isEmpty ? "" : "\(hex):\(vout)" + } + + var formattedAmount: String { + let dash = Double(amount) / 100_000_000.0 + return String(format: "%.8f DASH", dash) + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWallet.swift new file mode 100644 index 00000000000..20292cdcedd --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWallet.swift @@ -0,0 +1,94 @@ +import Foundation +import SwiftData + +// `PersistentWallet` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentWallet { + #Index([\.networkRaw], [\.walletGroupId]) + #Unique([\.walletId]) + + var walletId: Data + var walletGroupId: Data = Data() + var networkRaw: UInt32? + + var network: Network? { + get { + guard let raw = networkRaw else { return nil } + return Network(rawValue: raw) ?? .testnet + } + set { networkRaw = newValue?.rawValue } + } + var name: String? + var walletDescription: String? + var birthHeight: UInt32 + var syncedHeight: UInt32 + var lastSynced: UInt64 + var lastAppliedChainLockBytes: Data? + var isImported: Bool = false + var seedBindingVerifiedMarker: String? + var createdAt: Date + var lastUpdated: Date + + @Relationship(deleteRule: .cascade, inverse: \PersistentAccount.wallet) + var accounts: [PersistentAccount] + + @Relationship(deleteRule: .nullify, inverse: \PersistentIdentity.wallet) + var identities: [PersistentIdentity] + + init( + walletId: Data, + walletGroupId: Data = Data(), + network: Network? = nil, + name: String? = nil, + walletDescription: String? = nil, + birthHeight: UInt32 = 0, + syncedHeight: UInt32 = 0, + isImported: Bool = false + ) { + self.walletId = walletId + self.walletGroupId = walletGroupId + self.networkRaw = network?.rawValue + self.name = name + self.walletDescription = walletDescription + self.birthHeight = birthHeight + self.syncedHeight = syncedHeight + self.lastSynced = 0 + self.isImported = isImported + self.createdAt = Date() + self.lastUpdated = Date() + self.accounts = [] + self.identities = [] + } + } +} + +extension DashSchemaV1.PersistentWallet { + var label: String { + if let name = name, !name.isEmpty { + return name + } + let hex = walletId.prefix(4) + .map { String(format: "%02x", $0) } + .joined() + return hex.isEmpty ? "Wallet" : "Wallet \(hex)…" + } +} + +extension DashSchemaV1.PersistentWallet { + static func predicate(walletId: Data) -> Predicate { + #Predicate { $0.walletId == walletId } + } + + static func predicate( + walletGroupId: Data + ) -> Predicate { + #Predicate { $0.walletGroupId == walletGroupId } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWalletManagerMetadata.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWalletManagerMetadata.swift new file mode 100644 index 00000000000..183c6c8c41c --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWalletManagerMetadata.swift @@ -0,0 +1,34 @@ +import Foundation +import SwiftData + +// `PersistentWalletManagerMetadata` exactly as schema DashSchemaV1 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV1 { + @Model + final class PersistentWalletManagerMetadata { + @Attribute(.unique) var networkRaw: UInt32 + var combinedSyncHeight: UInt32 + var combinedSyncBlockHash: Data? + var walletCount: Int + var createdAt: Date + var lastUpdated: Date + + var network: Network { + get { Network(rawValue: networkRaw) ?? .testnet } + set { networkRaw = newValue.rawValue } + } + + init(network: Network) { + self.networkRaw = network.rawValue + self.combinedSyncHeight = 0 + self.walletCount = 0 + self.createdAt = Date() + self.lastUpdated = Date() + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+TokenTypes.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+TokenTypes.swift new file mode 100644 index 00000000000..c499469000b --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+TokenTypes.swift @@ -0,0 +1,153 @@ +import Foundation +import SwiftData + +// Inline value types exactly as schema DashSchemaV1 stored them, generated +// by scripts/freeze_schema_models.py from TokenTypes.swift +// at commit 5f58417079. SwiftData expands a stored Codable struct into composite +// attributes of the owning entity, so these shapes are inputs to that +// version's checksum just like the model's own properties. Do not edit. +extension DashSchemaV1 { + struct ChangeControlRules: Codable, Equatable, Sendable { + var authorizedToMakeChange: String + var adminActionTakers: String + var changingAuthorizedActionTakersToNoOneAllowed: Bool + var changingAdminActionTakersToNoOneAllowed: Bool + var selfChangingAdminActionTakersAllowed: Bool + + init( + authorizedToMakeChange: String = AuthorizedActionTakers.noOne.rawValue, + adminActionTakers: String = AuthorizedActionTakers.noOne.rawValue, + changingAuthorizedActionTakersToNoOneAllowed: Bool = false, + changingAdminActionTakersToNoOneAllowed: Bool = false, + selfChangingAdminActionTakersAllowed: Bool = false + ) { + self.authorizedToMakeChange = authorizedToMakeChange + self.adminActionTakers = adminActionTakers + self.changingAuthorizedActionTakersToNoOneAllowed = changingAuthorizedActionTakersToNoOneAllowed + self.changingAdminActionTakersToNoOneAllowed = changingAdminActionTakersToNoOneAllowed + self.selfChangingAdminActionTakersAllowed = selfChangingAdminActionTakersAllowed + } + + static func mostRestrictive() -> ChangeControlRules { + return ChangeControlRules() + } + + static func contractOwnerControlled() -> ChangeControlRules { + return ChangeControlRules( + authorizedToMakeChange: AuthorizedActionTakers.contractOwner.rawValue, + adminActionTakers: AuthorizedActionTakers.noOne.rawValue, + selfChangingAdminActionTakersAllowed: true + ) + } + } + + enum AuthorizedActionTakers: String, CaseIterable, Codable, Sendable { + case noOne = "NoOne" + case contractOwner = "ContractOwner" + case mainGroup = "MainGroup" + + static func identity(_ id: Data) -> String { + return "Identity:\(id.toBase58String())" + } + + static func group(_ position: Int) -> String { + return "Group:\(position)" + } + } + + struct TokenPerpetualDistribution: Codable, Equatable, Sendable { + var distributionType: String + var distributionRecipient: String + var enabled: Bool + var lastDistributionTime: Date? + var nextDistributionTime: Date? + + init(distributionRecipient: String = "AllEqualShare", enabled: Bool = true) { + self.distributionType = "{}" + self.distributionRecipient = distributionRecipient + self.enabled = enabled + } + } + + struct TokenPreProgrammedDistribution: Codable, Equatable, Sendable { + var distributionSchedule: [DistributionEvent] + var currentEventIndex: Int + var totalDistributed: String + var remainingToDistribute: String + var isActive: Bool + var isPaused: Bool + var isCompleted: Bool + + init() { + self.distributionSchedule = [] + self.currentEventIndex = 0 + self.totalDistributed = "0" + self.remainingToDistribute = "0" + self.isActive = true + self.isPaused = false + self.isCompleted = false + } + } + + struct DistributionEvent: Codable, Equatable, Sendable { + var id: UUID + var triggerType: String + var triggerTime: Date? + var triggerBlock: Int64? + var triggerCondition: String? + var amount: String + var recipient: String + var description: String? + + init(triggerTime: Date, amount: String, recipient: String = "AllHolders", description: String? = nil) { + self.id = UUID() + self.triggerType = "Time" + self.triggerTime = triggerTime + self.amount = amount + self.recipient = recipient + self.description = description + } + } + + struct TokenDistributionChangeRules: Codable, Equatable, Sendable { + var perpetualDistributionRules: ChangeControlRules? + var newTokensDestinationIdentityRules: ChangeControlRules? + var mintingAllowChoosingDestinationRules: ChangeControlRules? + var changeDirectPurchasePricingRules: ChangeControlRules? + + init( + perpetualDistributionRules: ChangeControlRules? = nil, + newTokensDestinationIdentityRules: ChangeControlRules? = nil, + mintingAllowChoosingDestinationRules: ChangeControlRules? = nil, + changeDirectPurchasePricingRules: ChangeControlRules? = nil + ) { + self.perpetualDistributionRules = perpetualDistributionRules + self.newTokensDestinationIdentityRules = newTokensDestinationIdentityRules + self.mintingAllowChoosingDestinationRules = mintingAllowChoosingDestinationRules + self.changeDirectPurchasePricingRules = changeDirectPurchasePricingRules + } + } + + enum TokenTradeMode: String, CaseIterable, Codable, Sendable { + case notTradeable = "NotTradeable" + + var displayName: String { + switch self { + case .notTradeable: + return "Not Tradeable" + } + } + } + + struct TokenLocalization: Codable, Equatable, Sendable { + let singularForm: String + let pluralForm: String + let description: String? + + init(singularForm: String, pluralForm: String, description: String? = nil) { + self.singularForm = singularForm + self.pluralForm = pluralForm + self.description = description + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV2+PersistentTrackedMasternode.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV2+PersistentTrackedMasternode.swift new file mode 100644 index 00000000000..a07502dc4d8 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV2+PersistentTrackedMasternode.swift @@ -0,0 +1,42 @@ +import Foundation +import SwiftData + +// `PersistentTrackedMasternode` exactly as schema DashSchemaV2 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV2 { + @Model + final class PersistentTrackedMasternode { + #Unique([\.networkRaw, \.proTxHash]) + #Index([\.networkRaw]) + + var networkRaw: UInt32 + var proTxHash: Data + var label: String? + var addedAt: UInt64 + var snapshotJSON: String + + var network: Network? { + get { Network(rawValue: networkRaw) } + set { networkRaw = newValue?.rawValue ?? networkRaw } + } + + init( + networkRaw: UInt32, + proTxHash: Data, + label: String?, + addedAt: UInt64, + snapshotJSON: String + ) { + self.networkRaw = networkRaw + self.proTxHash = proTxHash + self.label = label + self.addedAt = addedAt + self.snapshotJSON = snapshotJSON + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV3+PersistentAssetLock.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV3+PersistentAssetLock.swift new file mode 100644 index 00000000000..16db6310a7e --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV3+PersistentAssetLock.swift @@ -0,0 +1,102 @@ +import Foundation +import SwiftData + +// `PersistentAssetLock` exactly as schema DashSchemaV3 registered it, generated by +// scripts/freeze_schema_models.py from the live model at commit 5f58417079. +// Do not edit: every stored property, its optionality and default, and +// every @Attribute / @Relationship / #Index / #Unique here is an input to +// that version's checksum, and changing any of them re-breaks the stores +// this copy exists to keep openable. See the live model for what each +// column means. +extension DashSchemaV3 { + @Model + final class PersistentAssetLock { + #Index([\.walletId]) + + @Attribute(.unique) var outPointHex: String + + var walletId: Data + + var transactionBytes: Data + + var fundingTypeRaw: Int + + var identityIndexRaw: Int32 + + var accountIndexRaw: Int32 = 0 + + var amountDuffs: Int64 + + var statusRaw: Int + + var proofBytes: Data? + + var recipientPlatformAddressHash: Data? + + var recipientPlatformAddressType: UInt8? + + var recipientIsExternal: Bool? + + var createdAt: Date + var updatedAt: Date + + init( + outPointHex: String, + walletId: Data, + transactionBytes: Data, + fundingTypeRaw: Int, + identityIndexRaw: Int32, + accountIndexRaw: Int32 = 0, + amountDuffs: Int64, + statusRaw: Int, + proofBytes: Data? = nil + ) { + self.outPointHex = outPointHex + self.walletId = walletId + self.transactionBytes = transactionBytes + self.fundingTypeRaw = fundingTypeRaw + self.identityIndexRaw = identityIndexRaw + self.accountIndexRaw = accountIndexRaw + self.amountDuffs = amountDuffs + self.statusRaw = statusRaw + self.proofBytes = proofBytes + self.createdAt = Date() + self.updatedAt = Date() + } + } +} + +extension DashSchemaV3.PersistentAssetLock { + static func predicate(walletId: Data) -> Predicate { + #Predicate { entry in + entry.walletId == walletId + } + } + + static func predicate( + walletId: Data, + identityIndex: UInt32 + ) -> Predicate { + let identityIndexRaw = Int32(bitPattern: identityIndex) + return #Predicate { entry in + entry.walletId == walletId && entry.identityIndexRaw == identityIndexRaw + } + } +} + +extension DashSchemaV3.PersistentAssetLock { + static func encodeOutPoint(rawBytes: Data) -> String { + precondition(rawBytes.count == 36, "outpoint must be 36 bytes") + let txid = rawBytes.prefix(32) + let voutBytes = rawBytes.suffix(4) + let vout = voutBytes.withUnsafeBytes { raw -> UInt32 in + var value: UInt32 = 0 + withUnsafeMutableBytes(of: &value) { dst in + dst.copyBytes(from: raw.prefix(MemoryLayout.size)) + } + return UInt32(littleEndian: value) + } + let txidHex = txid.reversed().map { String(format: "%02x", $0) }.joined() + return "\(txidHex):\(vout)" + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentAssetLock.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentAssetLock.swift index e928306eb9c..676d7ebf599 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentAssetLock.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentAssetLock.swift @@ -199,8 +199,8 @@ public final class PersistentAssetLock { /// `ModelContainer(for:migrationPlan:configurations:)` would fail to /// open it with Cocoa error 134504 ("Cannot use staged migration with /// an unknown model version"). So V1 and V2 now reference a frozen - /// copy of this model (`DashSchemaV1.PersistentAssetLock`, in - /// `DashSchemaFrozenModels.swift`), this property is what schema + /// copy of this model (`DashSchemaV1.PersistentAssetLock`, generated + /// under `FrozenSchemas/`), this property is what schema /// `DashSchemaV3` adds, and a lightweight V2 -> V3 stage carries /// existing stores across. Do the same for the next property added /// here. diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift index 08d1f84c383..26665d563b2 100644 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift @@ -24,7 +24,7 @@ final class DashModelMigrationTests: XCTestCase { var v1Container: ModelContainer? = try ModelContainer( for: v1Schema, configurations: [v1Configuration]) - // V1 registers the FROZEN component (see `DashSchemaFrozenModels`), + // V1 registers the frozen graph (see `FrozenSchemas/`), // so a row written into a V1 container is that type — inserting the // live one would materialise as the frozen entity and then fail its // cast on read. @@ -51,7 +51,9 @@ final class DashModelMigrationTests: XCTestCase { FetchDescriptor()) XCTAssertEqual(keywords.map(\.keyword), ["preserved"]) - migrated.mainContext.insert(PersistentTrackedMasternode( + // V2 registers the frozen `PersistentTrackedMasternode`, so the row + // written into a V2 container is that type too. + migrated.mainContext.insert(DashSchemaV2.PersistentTrackedMasternode( networkRaw: Network.testnet.rawValue, proTxHash: Data(repeating: 7, count: 32), label: "new in V2", @@ -60,13 +62,13 @@ final class DashModelMigrationTests: XCTestCase { try migrated.mainContext.save() XCTAssertEqual( try migrated.mainContext.fetchCount( - FetchDescriptor()), + FetchDescriptor()), 1) } /// The stage this change adds: a V3 store must migrate to V4 and read /// back with the sweep columns backfilled to their "nothing swept yet" - /// values. V3 registers the frozen component, so the row goes in as the + /// values. V3 registers the frozen graph, so the row goes in as the /// frozen type and comes out as the live one — which is the whole point /// of the freeze: the same entity, one property wider. A pending-input /// row rides along so the tombstone index V4 adds is exercised by the @@ -160,7 +162,7 @@ final class DashModelMigrationTests: XCTestCase { /// The whole chain from the oldest registered version, on the models this /// change actually widens: a V1 store carrying a wallet, a transaction /// and a coin must arrive at V4 with every row intact and the V4 columns - /// at their backfill values. V1 and V2 register the frozen component, + /// at their backfill values. V1 and V2 register the frozen graph, /// so the rows go in as frozen types and come out live — the property /// the freeze exists to guarantee, pinned here where it matters most. @MainActor @@ -361,8 +363,10 @@ final class DashModelMigrationTests: XCTestCase { migrationPlan: DashMigrationPlan.self, configurations: [v3Configuration]) + // V3 registers the frozen `DashSchemaV3.PersistentAssetLock`, the + // shape that gained the column, so the read side is that type. let locks = try migrated.mainContext.fetch( - FetchDescriptor()) + FetchDescriptor()) XCTAssertEqual(locks.count, 1) let lock = try XCTUnwrap(locks.first) XCTAssertEqual(lock.outPointHex, outPointHex) @@ -376,7 +380,7 @@ final class DashModelMigrationTests: XCTestCase { lock.recipientIsExternal = true try migrated.mainContext.save() XCTAssertEqual( - try migrated.mainContext.fetch(FetchDescriptor()) + try migrated.mainContext.fetch(FetchDescriptor()) .first?.recipientIsExternal, true) } diff --git a/packages/swift-sdk/scripts/freeze_schema_models.py b/packages/swift-sdk/scripts/freeze_schema_models.py new file mode 100755 index 00000000000..c4bf15434bd --- /dev/null +++ b/packages/swift-sdk/scripts/freeze_schema_models.py @@ -0,0 +1,396 @@ +#!/usr/bin/env python3 +"""Generate the frozen SwiftData model copies for released schema versions. + +A `VersionedSchema` identifies a store by the checksum of the entities it +declares, so a released version may only reference model types whose shape +never changes again. Pointing a released version at a live `@Model` type +means the next property added to that type silently changes the released +checksum: a store written by the previously shipped build then matches no +registered version and fails to open with Cocoa error 134504 ("Cannot use +staged migration with an unknown model version") instead of migrating. + +This script copies each live `@Model` class as it existed at a given commit +into a nested type of the schema enum that version registers +(`extension DashSchemaV1 { final class PersistentX { ... } }`), one file per +model, under `Persistence/FrozenSchemas/`. SwiftData derives the entity name +from the unqualified type name, so `DashSchemaV1.PersistentX` and the live +`PersistentX` describe the same entity, which is what lets a migration stage +map one onto the other. + +The class body and the extensions declared in the model's own file are +copied; doc comments, `public` modifiers and top-level enums are dropped. +Extensions add no stored properties (so they are not part of the entity) +but the class body may call into them. The stored properties, their +optionality and defaults, +`@Attribute`, `@Relationship`, `#Index` and `#Unique` are what the checksum +hashes, and they are copied verbatim. + +Value types a model stores inline (Codable structs and raw enums SwiftData +expands into composite attributes, such as `ChangeControlRules` on +`PersistentToken`) are entity-hash inputs too, so they are frozen the same +way, into one nested file per schema, and every frozen model body then +resolves those names to the nested copies. Names are qualified per schema: +a model frozen only under `DashSchemaV2` that mentioned a `DashSchemaV1` +model by bare name in an extension would still bind to the live type. + +Every released version is frozen as a whole graph, never partially: a +relationship binds its destination by entity name, and SwiftData resolves +that name to whichever Swift type claimed it first in the process, so a +frozen model whose relationship pointed at a live type could be hashed with +the live type's current shape. + +`FREEZES` below is the record of what each released version registers and +the commit its shapes are taken from. Rows are append-only: retiring a +version means adding rows for it (normally one row listing every model at +the last commit before the change), adding the new `DashSchemaVN` and a +migration stage, and rerunning this script. Never edit an existing row; a +released checksum cannot move. + +Usage, from anywhere inside the repository: + + scripts/freeze_schema_models.py # regenerate every frozen file + scripts/freeze_schema_models.py --check # exit 1 if any file would change + +`--check` is what a reviewer or CI runs: the frozen files are a pure +function of `FREEZES` and the repository history, so a clean check proves +nobody edited a frozen copy by hand. +""" + +import argparse +import dataclasses +import os +import re +import subprocess +import sys + +MODELS_DIR = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models" +OUT_DIR = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas" +TOKEN_TYPES_FILE = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Types/TokenTypes.swift" + +# The value types `PersistentToken` stores inline, plus the enums their +# initializers reference. +TOKEN_VALUE_TYPES = [ + "ChangeControlRules", + "AuthorizedActionTakers", + "TokenPerpetualDistribution", + "TokenPreProgrammedDistribution", + "DistributionEvent", + "TokenDistributionChangeRules", + "TokenTradeMode", + "TokenLocalization", +] + +# Every model registered by the versions that share the V1 graph, in the +# order `DashModelContainer` lists them, minus the two that have their own +# rows below (`PersistentAssetLock`, whose shape differs between V2 and V3, +# and `PersistentTrackedMasternode`, which V2 added). +V1_GRAPH_MODELS = [ + "PersistentIdentity", + "PersistentDPNSName", + "PersistentDashpayProfile", + "PersistentDashpayContactProfile", + "PersistentDashpayContactRequest", + "PersistentDashpayPayment", + "PersistentDashpayIgnoredSender", + "PersistentDocument", + "PersistentDataContract", + "PersistentPublicKey", + "PersistentTokenBalance", + "PersistentKeyword", + "PersistentToken", + "PersistentDocumentType", + "PersistentIndex", + "PersistentProperty", + "PersistentTokenHistoryEvent", + "PersistentPlatformAddress", + "PersistentPlatformAddressesSyncState", + "PersistentWallet", + "PersistentAccount", + "PersistentCoreAddress", + "PersistentTransaction", + "PersistentTxo", + "PersistentPendingInput", + "PersistentWalletManagerMetadata", + "PersistentShieldedNote", + "PersistentShieldedOutgoingNote", + "PersistentShieldedSyncState", + "PersistentShieldedActivity", + "PersistentShieldedViewingKey", + "PersistentInvitation", + "PersistentMasternode", +] + + +@dataclasses.dataclass(frozen=True) +class Freeze: + """One schema's copy of some models, taken from one commit.""" + + schema: str + commit: str + models: tuple + value_types_file: str = "" + value_types: tuple = () + + +FREEZES = [ + # The asset lock as V1 and V2 shipped it: the last commit before + # `recipientIsExternal` was added to the live model. + Freeze("DashSchemaV1", "7127c38566", ("PersistentAssetLock",)), + # The rest of the graph, shared by V1, V2 and V3, at the last commit + # before V4 widened the wallet transaction models. + Freeze( + "DashSchemaV1", + "5f58417079", + tuple(V1_GRAPH_MODELS), + TOKEN_TYPES_FILE, + tuple(TOKEN_VALUE_TYPES), + ), + # V2 adds the tracked-masternode registry. + Freeze("DashSchemaV2", "5f58417079", ("PersistentTrackedMasternode",)), + # V3 replaces the asset lock with the shape that has `recipientIsExternal`. + Freeze("DashSchemaV3", "5f58417079", ("PersistentAssetLock",)), +] + +HEADER = "import Foundation\nimport SwiftData\n\n" + + +def git(root, *args): + try: + return subprocess.check_output( + ["git", *args], text=True, encoding="utf-8", cwd=root + ) + except subprocess.CalledProcessError as error: + raise SystemExit( + f"git {' '.join(args)} failed (exit {error.returncode}); a commit named in " + "FREEZES may not be fetched locally" + ) + + +def repo_root(): + return git(os.getcwd(), "rev-parse", "--show-toplevel").strip() + + +def strip_comments(lines): + """Drop comment lines and `public`, collapsing the blank runs left behind.""" + out = [] + for line in lines: + stripped = line.strip() + if stripped.startswith("///") or stripped.startswith("//"): + continue + line = re.sub(r"\bpublic(\(set\))? ", "", line) + if line.strip() == "" and out and out[-1].strip() == "": + continue + out.append(line) + while out and out[-1].strip() == "": + out.pop() + return out + + +def block_end(lines, start): + """Index of the line closing the brace block that opens at `start`. + + Braces on `//` comment lines are ignored so a `// { ... }` note inside + a body cannot truncate the copy. Block comments are not parsed, so one + is refused rather than risk a silently truncated copy. + """ + depth = 0 + opened = False + for i in range(start, len(lines)): + stripped = lines[i].strip() + if stripped.startswith("//"): + continue + if "/*" in lines[i]: + raise SystemExit(f"line {i + 1}: block comments are not supported") + depth += lines[i].count("{") - lines[i].count("}") + opened = opened or "{" in lines[i] + if depth == 0 and opened: + return i + raise SystemExit("unbalanced braces") + + +def extract_value_type(source, name): + """A top-level `struct ` / `enum ` block, comments stripped.""" + lines = source.splitlines() + for i, line in enumerate(lines): + if re.match(rf"^(public )?(struct|enum) {name}\b", line): + return strip_comments(lines[i : block_end(lines, i) + 1]) + raise SystemExit(f"{name}: no top-level struct or enum found") + + +def extract_class(source, model): + """The `@Model ... final class { ... }` block, comments stripped.""" + lines = source.splitlines() + start = None + for i, line in enumerate(lines): + if re.match(rf"^(public )?final class {model}\b", line): + start = i + break + if start is None: + raise SystemExit(f"{model}: no top-level class found") + # Include the macro attributes directly above the class (`@Model`), + # looking past doc comments between them and the class. + while start > 0 and ( + lines[start - 1].startswith("@") or lines[start - 1].startswith("///") + ): + start -= 1 + end = block_end(lines, start) + return strip_comments(lines[start : end + 1]) + + +def extract_extensions(source, model): + """Every top-level `extension { ... }` body, comments stripped.""" + lines = source.splitlines() + bodies = [] + for i, line in enumerate(lines): + if not re.match(rf"^(public )?extension {model}\b", line): + continue + if not re.match(rf"^(public )?extension {model}\s*(:[^{{]*)?\{{", line): + raise SystemExit( + f"{model}: line {i + 1}: an extension header must open its brace " + "on the same line" + ) + end = block_end(lines, i) + inner = strip_comments(lines[i + 1 : end]) + conformance = re.search(r"extension \w+\s*(:[^{]*)\{", line) + bodies.append(((conformance.group(1).strip() if conformance else ""), inner)) + return bodies + + +def indent(body): + return "\n".join((" " + line) if line.strip() else "" for line in body) + + +def render_value_types(freeze, sha, source): + bodies = [indent(extract_value_type(source, name)) for name in freeze.value_types] + return ( + HEADER + + f"// Inline value types exactly as schema {freeze.schema} stored them, generated\n" + f"// by scripts/freeze_schema_models.py from {os.path.basename(freeze.value_types_file)}\n" + f"// at commit {sha}. SwiftData expands a stored Codable struct into composite\n" + "// attributes of the owning entity, so these shapes are inputs to that\n" + "// version's checksum just like the model's own properties. Do not edit.\n" + f"extension {freeze.schema} {{\n" + + "\n\n".join(bodies) + + "\n}\n" + ) + + +def render_model(freeze, sha, source, model, sibling): + # An extension of a nested type does not see its sibling nested types + # by bare name (that lookup lands on the live top-level type), so model + # and frozen value-type names inside extension bodies are qualified. + extensions = "" + for conformance, inner in extract_extensions(source, model): + inner = [sibling.sub(rf"{freeze.schema}.\1", line) for line in inner] + extensions += ( + f"\nextension {freeze.schema}.{model}{' ' + conformance if conformance else ''} {{\n" + + "\n".join(inner) + + "\n}\n" + ) + return ( + HEADER + + f"// `{model}` exactly as schema {freeze.schema} registered it, generated by\n" + f"// scripts/freeze_schema_models.py from the live model at commit {sha}.\n" + "// Do not edit: every stored property, its optionality and default, and\n" + "// every @Attribute / @Relationship / #Index / #Unique here is an input to\n" + "// that version's checksum, and changing any of them re-breaks the stores\n" + "// this copy exists to keep openable. See the live model for what each\n" + "// column means.\n" + f"extension {freeze.schema} {{\n" + f"{indent(extract_class(source, model))}\n" + "}\n" + + extensions + ) + + +def render_all(root): + """Every frozen file as {relative path: text}.""" + # Names frozen under a schema, across all of its rows: any of them + # mentioned inside an extension body must resolve to the nested copy. + per_schema = {} + for freeze in FREEZES: + per_schema.setdefault(freeze.schema, set()).update(freeze.models) + per_schema[freeze.schema].update(freeze.value_types) + + files = {} + + def emit(path, text): + if path in files: + raise SystemExit(f"{path}: produced by two FREEZES rows") + files[path] = text + + for freeze in FREEZES: + sha = git(root, "rev-parse", "--verify", freeze.commit).strip()[:10] + sibling = re.compile( + r"(? Date: Tue, 8 Sep 2026 18:31:06 +0700 Subject: [PATCH 02/12] test(swift-sdk): migrate stores that older builds actually wrote The migration tests built their source stores in this process from the same `Schema(versionedSchema:)` they then verified. SwiftData binds an entity name to the first Swift type that claims it, so a frozen version whose entities had silently rebound to a live shape would round-trip itself and pass; only a store from a build that knew nothing of the live shape can tell. Fixtures/SchemaStores/dash-v{1,2,3}.store were written by a build of the SDK at 96a103375e, the last state of the persistence sources before V4, through that build's own DashSchemaV1/V2/V3. Each carries a wallet, an account, a core address, two transactions, a TXO linked to both, a pending input, an identity, a keyword, an asset lock and, from V2, a tracked masternode. Two tests open them: one through `DashModelContainer.create`'s exact order (live schema first, then the migration plan) and reads every row back through the live types with relationships intact and the V4 columns at their backfill values; the other builds each frozen version after the live schema and compares its per-entity `NSStoreModelVersionHashes` and checksum with the store its build wrote. The in-process tests stay. Test would have caught a frozen-shape drift in CI: with one stored property added to `DashSchemaV1.PersistentWallet`, the two fixture tests fail (hash drift on PersistentWallet; open fails with NSCocoaErrorDomain 134504) while all six in-process tests still pass; with the generated copy restored, 8/8 pass. Co-Authored-By: Claude Fable 5.1 --- packages/swift-sdk/Package.swift | 5 +- .../DashModelMigrationTests.swift | 214 ++++++++++++++++++ .../Fixtures/SchemaStores/dash-v1.store | Bin 0 -> 643072 bytes .../Fixtures/SchemaStores/dash-v2.store | Bin 0 -> 659456 bytes .../Fixtures/SchemaStores/dash-v3.store | Bin 0 -> 659456 bytes 5 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 packages/swift-sdk/SwiftTests/SwiftDashSDKTests/Fixtures/SchemaStores/dash-v1.store create mode 100644 packages/swift-sdk/SwiftTests/SwiftDashSDKTests/Fixtures/SchemaStores/dash-v2.store create mode 100644 packages/swift-sdk/SwiftTests/SwiftDashSDKTests/Fixtures/SchemaStores/dash-v3.store diff --git a/packages/swift-sdk/Package.swift b/packages/swift-sdk/Package.swift index 253d84fcccd..496f8eb5408 100644 --- a/packages/swift-sdk/Package.swift +++ b/packages/swift-sdk/Package.swift @@ -32,7 +32,10 @@ let package = Package( .testTarget( name: "SwiftDashSDKTests", dependencies: ["SwiftDashSDK"], - path: "SwiftTests/SwiftDashSDKTests" + path: "SwiftTests/SwiftDashSDKTests", + // Persistent stores written by the builds that shipped each + // released schema version; `DashModelMigrationTests` opens them. + resources: [.copy("Fixtures")] ), // Integration tests against a local dashmate devnet. diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift index 26665d563b2..2da7642c89c 100644 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift @@ -1,10 +1,224 @@ +import CoreData import Foundation import SwiftData import XCTest @testable import SwiftDashSDK +/// Migration coverage from two directions: source stores built in this +/// process from each registered version, and stores that OLDER BUILDS +/// actually wrote. +/// +/// The fixture stores under `Fixtures/SchemaStores/` were written by a build +/// of the persistence sources as of commit 5f58417079 — the last state +/// before V4, the state the frozen copies under `FrozenSchemas/` are +/// generated from — through that build's own `DashSchemaV1` / +/// `DashSchemaV2` / `DashSchemaV3`. They pin the frozen copies as that +/// pre-V4 build defined them, not what the original V1 release wrote (see +/// the `DashSchemaV1` doc for why those stores are expected to fail open +/// and be rebuilt). Each carries a wallet, +/// an account, a core address, two transactions, a TXO linked to both, a +/// pending input, an identity, a keyword, an asset lock and (from V2) a +/// tracked masternode — enough to exercise every relationship in the wallet +/// graph. +/// +/// A source store written in this process by `Schema(versionedSchema:)` +/// cannot replace them: SwiftData binds an entity name to the first Swift +/// type that claims it, so such a store carries whatever shape the process +/// had already bound, and a frozen version whose entities had silently +/// rebound to the live shape would round-trip itself and pass vacuously. +/// Only a store from a build that knew nothing of the live shape can tell. final class DashModelMigrationTests: XCTestCase { + /// SwiftData binds an entity name to the first Swift type that claims it + /// in the process, so whether the live schema is built before or after + /// a frozen version is decided once per process, not per test. Build it + /// first here, as `DashModelContainer.create` does, so every test below + /// runs in the order the app would. + override class func setUp() { + super.setUp() + _ = DashModelContainer.schema + } + + private struct Fixture { + let name: String + let version: any VersionedSchema.Type + let hasTrackedMasternode: Bool + let assetLockRecipientIsExternal: Bool? + } + + private static let fixtures: [Fixture] = [ + Fixture( + name: "dash-v1", version: DashSchemaV1.self, + hasTrackedMasternode: false, assetLockRecipientIsExternal: nil), + Fixture( + name: "dash-v2", version: DashSchemaV2.self, + hasTrackedMasternode: true, assetLockRecipientIsExternal: nil), + Fixture( + name: "dash-v3", version: DashSchemaV3.self, + hasTrackedMasternode: true, assetLockRecipientIsExternal: true), + ] + + private static let fixtureWalletId = Data(repeating: 0x31, count: 32) + private static let fixtureSpendTxid = Data(repeating: 0x32, count: 32) + private static let fixtureFundingTxid = Data(repeating: 0x34, count: 32) + private static let fixtureIdentityId = Data(repeating: 0x35, count: 32) + + /// A private, writable copy of a fixture store. + private func copyFixture(_ fixture: Fixture) throws -> (URL, URL) { + let source = try XCTUnwrap( + Bundle.module.url( + forResource: fixture.name, withExtension: "store", + subdirectory: "Fixtures/SchemaStores"), + "missing fixture \(fixture.name).store") + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory( + at: directory, withIntermediateDirectories: true) + let copy = directory.appendingPathComponent("\(fixture.name).store") + try FileManager.default.copyItem(at: source, to: copy) + return (directory, copy) + } + + private static func storeHashes(at url: URL) throws -> (String, [String: Data]) { + let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( + type: .sqlite, at: url) + let checksum = try XCTUnwrap( + metadata["NSStoreModelVersionChecksumKey"] as? String, + "store has no model checksum") + let hashes = try XCTUnwrap( + metadata["NSStoreModelVersionHashes"] as? [String: Data], + "store has no entity hashes") + return (checksum, hashes) + } + + /// Every fixture opens through `DashModelContainer.create`'s exact + /// order — live schema built first, then the migration plan — and its + /// rows come back through the live types with the relationships intact + /// and the new columns at their migration defaults. + @MainActor + func testStoresWrittenByOlderBuildsMigrateThroughTheContainerFactory() throws { + for fixture in Self.fixtures { + let (directory, url) = try copyFixture(fixture) + defer { try? FileManager.default.removeItem(at: directory) } + + let container: ModelContainer + do { + container = try DashModelContainer.create(url: url) + } catch { + XCTFail("\(fixture.name): migration failed to open: \(error)") + continue + } + let context = container.mainContext + + let wallets = try context.fetch(FetchDescriptor()) + XCTAssertEqual(wallets.map(\.walletId), [Self.fixtureWalletId], fixture.name) + let wallet = try XCTUnwrap(wallets.first) + XCTAssertEqual(wallet.name, "fixture wallet", fixture.name) + XCTAssertEqual(wallet.syncedHeight, 120, fixture.name) + XCTAssertNil(wallet.lastAppliedChainLockHeight, fixture.name) + XCTAssertEqual(wallet.accounts.count, 1, fixture.name) + XCTAssertEqual( + wallet.identities.map(\.identityId), [Self.fixtureIdentityId], fixture.name) + + let accounts = try context.fetch(FetchDescriptor()) + let account = try XCTUnwrap(accounts.first, fixture.name) + XCTAssertEqual(accounts.count, 1, fixture.name) + XCTAssertEqual(account.wallet.walletId, Self.fixtureWalletId, fixture.name) + XCTAssertEqual(account.coreAddresses.map(\.address), ["yFixtureAddress"], fixture.name) + XCTAssertEqual( + Set(account.involvedTransactions.map(\.txid)), + [Self.fixtureSpendTxid, Self.fixtureFundingTxid], fixture.name) + + let transactions = try context.fetch(FetchDescriptor()) + XCTAssertEqual(transactions.count, 2, fixture.name) + let funding = try XCTUnwrap( + transactions.first { $0.txid == Self.fixtureFundingTxid }, fixture.name) + let spend = try XCTUnwrap( + transactions.first { $0.txid == Self.fixtureSpendTxid }, fixture.name) + XCTAssertEqual(funding.outputs.count, 1, fixture.name) + XCTAssertEqual(spend.inputs.count, 1, fixture.name) + XCTAssertEqual(spend.pendingInputs.count, 1, fixture.name) + + let txos = try context.fetch(FetchDescriptor()) + XCTAssertEqual(txos.count, 1, fixture.name) + let txo = try XCTUnwrap(txos.first) + XCTAssertEqual(txo.amount, 1_000, fixture.name) + XCTAssertEqual(txo.transaction?.txid, Self.fixtureFundingTxid, fixture.name) + XCTAssertEqual(txo.spendingTransaction?.txid, Self.fixtureSpendTxid, fixture.name) + XCTAssertEqual(txo.coreAddress?.address, "yFixtureAddress", fixture.name) + XCTAssertEqual(txo.account?.accountIndex, 0, fixture.name) + XCTAssertNil(txo.supersededByTxid, fixture.name) + + let pendingInputs = try context.fetch(FetchDescriptor()) + XCTAssertEqual(pendingInputs.count, 1, fixture.name) + let pending = try XCTUnwrap(pendingInputs.first) + XCTAssertEqual(pending.spendingTxid, Self.fixtureSpendTxid, fixture.name) + XCTAssertEqual(pending.spendingTransaction?.txid, Self.fixtureSpendTxid, fixture.name) + XCTAssertFalse(pending.isSweptTombstone, fixture.name) + XCTAssertNil(pending.winnerMinedHeight, fixture.name) + + let identities = try context.fetch(FetchDescriptor()) + XCTAssertEqual(identities.map(\.identityId), [Self.fixtureIdentityId], fixture.name) + XCTAssertEqual(identities.first?.balance, 5, fixture.name) + XCTAssertEqual(identities.first?.wallet?.walletId, Self.fixtureWalletId, fixture.name) + + let keywords = try context.fetch(FetchDescriptor()) + XCTAssertEqual(keywords.map(\.keyword), ["preserved"], fixture.name) + + let locks = try context.fetch(FetchDescriptor()) + XCTAssertEqual(locks.count, 1, fixture.name) + XCTAssertEqual(locks.first?.amountDuffs, 100_000, fixture.name) + XCTAssertEqual( + locks.first?.recipientIsExternal, fixture.assetLockRecipientIsExternal, + fixture.name) + + let tracked = try context.fetchCount(FetchDescriptor()) + XCTAssertEqual(tracked, fixture.hasTrackedMasternode ? 1 : 0, fixture.name) + + // The migrated store is writable through the new columns. + wallet.lastAppliedChainLockHeight = 130 + txo.supersededByTxid = Data(repeating: 0x36, count: 32) + try context.save() + XCTAssertEqual( + try context.fetch(FetchDescriptor()).first? + .lastAppliedChainLockHeight, + 130, fixture.name) + } + } + + /// Each frozen version, built after the live schema (the order + /// `DashModelContainer.create` uses, established process-wide in + /// `setUp`), still hashes every entity exactly as the build that shipped + /// it did. A partial freeze cannot give this: a frozen wallet reached + /// from a live `PersistentAccount.wallet` is rebound to the live + /// wallet's shape the moment the live schema is built first, and the + /// released checksum moves with it. + func testFrozenVersionsBuiltAfterTheLiveSchemaHashLikeTheStoresTheyShipped() throws { + for fixture in Self.fixtures { + let (directory, url) = try copyFixture(fixture) + defer { try? FileManager.default.removeItem(at: directory) } + let (shippedChecksum, shippedHashes) = try Self.storeHashes(at: url) + + let scratch = directory.appendingPathComponent("scratch.store") + let schema = Schema(versionedSchema: fixture.version) + let configuration = ModelConfiguration( + "DashSchemaScratch", schema: schema, url: scratch, allowsSave: true, + cloudKitDatabase: .none) + _ = try ModelContainer(for: schema, configurations: [configuration]) + let (builtChecksum, builtHashes) = try Self.storeHashes(at: scratch) + + let drifted = shippedHashes.keys.filter { shippedHashes[$0] != builtHashes[$0] } + .sorted() + XCTAssertEqual( + drifted, [], + "\(fixture.name): entities whose frozen shape no longer matches the shipped store") + XCTAssertEqual( + Set(builtHashes.keys), Set(shippedHashes.keys), + "\(fixture.name): entity membership differs from the shipped store") + XCTAssertEqual(builtChecksum, shippedChecksum, "\(fixture.name): checksum") + } + } + @MainActor func testV1StoreMigratesToV2AndAcceptsTrackedMasternodes() throws { let directory = FileManager.default.temporaryDirectory diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/Fixtures/SchemaStores/dash-v1.store b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/Fixtures/SchemaStores/dash-v1.store new file mode 100644 index 0000000000000000000000000000000000000000..48ff9ee6a67a475c6cf6d8021d6070616bfac86a GIT binary patch literal 643072 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

w8JWuC(1-47bhoL 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ȈlXo0uX=z1Rwwb2tWV=5P$## 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 Date: Wed, 9 Sep 2026 16:18:11 +0700 Subject: [PATCH 03/12] fix(swift-sdk): refuse an incomplete schema freeze The generator trusted its FREEZES table. A relationship target or a stored Codable value type left out of the table was simply not generated; the frozen model's bare name then bound to the live type, `--check` passed, and the released checksum moved with the live type's next change, so a store written by the shipped build failed to open with Cocoa error 134504. That is worse than the hand-written file it replaced, because generated output reads as authoritative. Before anything is generated or checked the table is now validated as a closed graph and refused, naming every missing type, if it is not: - every type a frozen model or frozen value type stores (a relationship target, an inline struct or enum, transitively through nested types) must be frozen under the same schema; - every `DashSchemaVN.X` that DashModelContainer.swift registers must be produced by the table, every frozen model must be registered, no released version may register a live model type, and only the newest version may use the live `modelTypes` list. Stored declarations are found by a small parser that skips computed and `@Transient` properties, walks nested types, and ignores comments and string literals; a type it cannot classify is refused rather than assumed native. Tests would have caught this in CI: run against the previous generator, omitting `PersistentTxo` (a relationship target), `ChangeControlRules` (a stored value type) and `DistributionEvent` (reached only through another value type) each fail with "SystemExit not raised"; with the fix all three are refused with the type named. The committed table validates clean and regenerates every frozen file byte-identical. Co-Authored-By: Claude Fable 5.1 --- .../swift-sdk/scripts/freeze_schema_models.py | 330 ++++++++++++++++-- .../scripts/test_freeze_schema_models.py | 319 +++++++++++++++++ 2 files changed, 628 insertions(+), 21 deletions(-) create mode 100644 packages/swift-sdk/scripts/test_freeze_schema_models.py diff --git a/packages/swift-sdk/scripts/freeze_schema_models.py b/packages/swift-sdk/scripts/freeze_schema_models.py index c4bf15434bd..b601a800f52 100755 --- a/packages/swift-sdk/scripts/freeze_schema_models.py +++ b/packages/swift-sdk/scripts/freeze_schema_models.py @@ -46,14 +46,33 @@ migration stage, and rerunning this script. Never edit an existing row; a released checksum cannot move. +The table is not trusted to be complete. Before anything is generated or +checked, the freeze is validated as a closed graph, and an incomplete one +is refused with the missing names rather than emitted partially: + + - every type a frozen model or frozen value type stores (a relationship + target, an inline Codable struct or enum) must itself be frozen under + the same schema, transitively; a bare name in a nested class body that + has no nested sibling resolves to the live type, which is exactly the + silent drift the freeze exists to prevent; + - every `DashSchemaVN.X` that `DashModelContainer.swift` registers must be + produced by the table, every frozen model must be registered, and no + released version may register a live model type. + Usage, from anywhere inside the repository: scripts/freeze_schema_models.py # regenerate every frozen file scripts/freeze_schema_models.py --check # exit 1 if any file would change -`--check` is what a reviewer or CI runs: the frozen files are a pure -function of `FREEZES` and the repository history, so a clean check proves -nobody edited a frozen copy by hand. +`--check` is what a reviewer or CI runs (the `swift-sdk-frozen-schema` job +in `.github/workflows/tests.yml`): the frozen files are a pure function of +`FREEZES` and the repository history, so a clean check proves nobody edited +a frozen copy by hand. It needs the commits named in `FREEZES` to be +present locally, so CI runs it on a full-history checkout. + +The generator's own tests, from the repository root: + + python3 -m unittest discover -s packages/swift-sdk/scripts -p 'test_*.py' """ import argparse @@ -66,6 +85,35 @@ MODELS_DIR = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models" OUT_DIR = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas" TOKEN_TYPES_FILE = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Types/TokenTypes.swift" +CONTAINER_FILE = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift" + +# Types SwiftData stores natively: they carry no shape of their own, so a +# stored property of one of these adds nothing that has to be frozen. +BUILTIN_TYPES = { + "Bool", + "Data", + "Date", + "Decimal", + "Double", + "Float", + "Int", + "Int8", + "Int16", + "Int32", + "Int64", + "String", + "UInt", + "UInt8", + "UInt16", + "UInt32", + "UInt64", + "URL", + "UUID", + "Array", + "Dictionary", + "Optional", + "Set", +} # The value types `PersistentToken` stores inline, plus the enums their # initializers reference. @@ -186,23 +234,33 @@ def strip_comments(lines): return out -def block_end(lines, start): - """Index of the line closing the brace block that opens at `start`. +def code_only(line): + """`line` with string literal contents and any trailing `//` comment removed. - Braces on `//` comment lines are ignored so a `// { ... }` note inside - a body cannot truncate the copy. Block comments are not parsed, so one - is refused rather than risk a silently truncated copy. + What remains is what brace counting and declaration matching may look + at: a `{` or a type name inside a string or a comment is not code. + Block comments are not parsed, so one is refused rather than risk a + silently truncated copy. """ + if "/*" in line: + raise SystemExit(f"block comments are not supported: {line.strip()}") + line = re.sub(r'"(?:\\.|[^"\\])*"', '""', line) + return line.split("//", 1)[0] + + +def braces(line): + """Net brace depth change of one line of code.""" + line = code_only(line) + return line.count("{") - line.count("}") + + +def block_end(lines, start): + """Index of the line closing the brace block that opens at `start`.""" depth = 0 opened = False for i in range(start, len(lines)): - stripped = lines[i].strip() - if stripped.startswith("//"): - continue - if "/*" in lines[i]: - raise SystemExit(f"line {i + 1}: block comments are not supported") - depth += lines[i].count("{") - lines[i].count("}") - opened = opened or "{" in lines[i] + depth += braces(lines[i]) + opened = opened or "{" in code_only(lines[i]) if depth == 0 and opened: return i raise SystemExit("unbalanced braces") @@ -256,6 +314,182 @@ def extract_extensions(source, model): return bodies +ATTRIBUTE = r"@\w+(\((?:[^()]|\([^()]*\))*\))?\s*" +MODIFIER = ( + r"(?:(?:private|fileprivate|internal|public|open|package)(?:\(set\))?" + r"|static|lazy|weak|unowned|final)\s+" +) +STORED_DECLARATION = re.compile( + rf"^\s*(?P(?:{ATTRIBUTE})*)(?P(?:{MODIFIER})*)(var|let)\s+\w+\s*" + r"(?::\s*(?P[^={]+))?(?:=\s*(?P[^{]*))?" +) +ENUM_CASE = re.compile(r"^\s*(indirect\s+)?case\s+(?P.+)") +NESTED_TYPE = re.compile( + rf"^\s*(?:{MODIFIER})*(?:indirect\s+)?(struct|enum|class)\s+(?P\w+)" +) + + +def type_names(expression): + """Capitalised identifiers a type expression or enum payload mentions. + + String literals are dropped (an enum raw value is not a type) and so is + everything after a dot: `Foo.Bar` belongs to `Foo`, and the nested type + is copied along with it. + """ + expression = re.sub(r'"[^"]*"', "", expression) + expression = re.sub(r"\.\w+", "", expression) + return set(re.findall(r"\b[A-Z]\w*", expression)) + + +def stored_type_names(block): + """Type names the stored declarations of one comment-stripped type body mention. + + A stored property (`var` / `let` with no getter body and no `@Transient`) + or an enum case payload is what SwiftData hashes, so those are the + references that must resolve to frozen copies. Types declared nested in + the body are copied with it, so their names are not returned, but their + own stored declarations are walked: a nested struct's field binds a + name the same way the model's does. Computed properties, initializers, + methods, static members and the raw-value clause do not affect the + entity, and a builtin has no shape to freeze, so none of those are + returned. A property whose type is inferred from its initializer + contributes the initializer's type names instead. + """ + names, nested = set(), set() + depth = 0 + attributes = "" + for i, raw in enumerate(block): + line = code_only(raw) + if depth == 1: + declared = NESTED_TYPE.match(line) + if declared: + nested.add(declared.group("name")) + names |= stored_type_names(block[i : block_end(block, i) + 1]) + if line.strip().startswith("@") and not re.search(r"\b(var|let)\b", line): + attributes += line + else: + following = next((code_only(l) for l in block[i + 1 :] if l.strip()), "") + if re.search(r"\b(var|let)\s+\w+\s*:\s*$", line): + line += " " + following.strip() + stored = STORED_DECLARATION.match(line) + if stored and "static" not in stored.group("modifiers"): + observed = re.search(r"\b(didSet|willSet)\b", line + " " + following) + computed = "{" in line and not observed + transient = "@Transient" in attributes + stored.group("attributes") + if not computed and not transient: + names |= type_names( + stored.group("type") or stored.group("initializer") or "" + ) + case = ENUM_CASE.match(line) + if case: + names |= type_names(case.group("payload")) + attributes = "" + depth += braces(raw) + return names - nested - BUILTIN_TYPES + + +def model_names_at(root, commit): + """Every `@Model` class under MODELS_DIR as of `commit`.""" + grep = subprocess.run( + ["git", "grep", "-l", "@Model", commit, "--", MODELS_DIR], + text=True, + encoding="utf-8", + cwd=root, + capture_output=True, + ) + if grep.returncode > 1: + raise SystemExit(f"git grep at {commit} failed: {grep.stderr.strip()}") + return { + os.path.basename(line.split(":", 1)[1])[: -len(".swift")] + for line in grep.stdout.splitlines() + } + + +def closure_problems(frozen, bodies, models_at): + """Why the freeze is not closed under "stores", as human-readable lines. + + `frozen` maps a schema to every name frozen under it, `bodies` maps + `(schema, name)` to the comment-stripped body of that frozen type plus + the names of the types its own extensions declare (those are copied + with it), and `models_at` maps a schema to the live model names at the + commits its rows are taken from. A stored reference must resolve to a + frozen sibling; a bare name with no sibling binds to the live type + instead, and its next change would move the released checksum. + """ + problems = [] + for (schema, owner), (body, declared) in sorted(bodies.items()): + for name in sorted(stored_type_names(body) - frozen[schema] - declared): + what = "a model" if name in models_at[schema] else "a value type" + problems.append( + f"{schema}.{owner} stores {name}, which is not frozen under {schema}: " + f"add it to a {schema} row of FREEZES as {what}" + ) + return problems + + +def registration_problems(container_lines, frozen_models, live_models): + """Why DashModelContainer.swift and FREEZES disagree, as human-readable lines. + + Every `DashSchemaVN.X` the container names must be produced by the + table, every frozen model must be named somewhere (else it is a copy no + version registers), a live model type may appear only inside the live + list `modelTypes`, and only the newest `DashSchemaVN` may use that + list: a released version that registered a live shape would hash it + into its checksum. + + This is a presence check, not per-version membership: a version that + registered the wrong frozen copy (say V2 taking V3's asset lock) passes + here and is caught by `DashModelMigrationTests` opening real stores. + """ + # Comments are blanked line by line rather than dropped so that a + # reported line number is the file's own. + lines = [code_only(line) for line in container_lines] + # The live list is found by its exact declaration; a reformat that + # hides it makes the check refuse rather than pass. + live_list = range(0) + versions = {} + for i, line in enumerate(lines): + if re.match(r"^\s*(public )?static var modelTypes: \[any PersistentModel\.Type\] \{", line): + live_list = range(i, block_end(lines, i) + 1) + version = re.match(r"^\s*(public )?enum DashSchemaV(\d+)\b", line) + if version: + versions[int(version.group(2))] = range(i, block_end(lines, i) + 1) + problems = [] + if not live_list: + problems.append(f"{CONTAINER_FILE}: the live model list `modelTypes` was not found") + # Only the newest version may be the live one; an older version that + # aliased `modelTypes` would register every live shape at once. + live_version = versions[max(versions)] if versions else range(0) + registered = set() + for i, line in enumerate(lines): + for schema, name in re.findall(r"\b(DashSchemaV\d+)\.([A-Z]\w*)\b", line): + registered.add((schema, name)) + if i in live_list: + continue + aliased = versions and re.search(r"\bmodelTypes\b", line) + if aliased and i not in live_version: + problems.append( + f"{CONTAINER_FILE} line {i + 1} uses the live model list `modelTypes` " + f"outside DashSchemaV{max(versions)}; a released version may only " + "register frozen copies" + ) + for name in re.findall(r"(? Result { fatalError() } + } + """ + self.assertEqual(self.names(body), set()) + + def test_access_modifiers_and_inferred_types_still_count(self): + body = """ + @Model + final class PersistentThing { + private var rules: ChangeControlRules? + private(set) var localization: TokenLocalization? + var tradeMode = TokenTradeMode.notTradeable + let events = [DistributionEvent]() + var stamp = Date() + static let shared: Registry = Registry() + @Attribute(.transformable(by: ColorTransformer.self)) var color: ColorBox + var observed: Counter { + didSet { print("changed") } + } + } + """ + self.assertEqual( + self.names(body), + { + "ChangeControlRules", + "TokenLocalization", + "TokenTradeMode", + "DistributionEvent", + "ColorBox", + "Counter", + }, + ) + + def test_fields_of_a_nested_type_are_walked(self): + body = """ + @Model + final class PersistentThing { + var meta: Meta? + struct Meta: Codable { + var rules: ChangeControlRules + var kind: Kind + enum Kind: String, Codable { + case a(DistributionEvent) + } + } + } + """ + self.assertEqual(self.names(body), {"ChangeControlRules", "DistributionEvent"}) + + def test_transient_comments_strings_and_split_declarations(self): + body = """ + @Model + final class PersistentThing { + @Transient var cache: LiveCache? = nil + var id: Data // Base58 identity id, see IdentityEntry + @Relationship(deleteRule: .cascade) + var outputs: + [PersistentTxo] = [] + init() { let brace = "{"; _ = brace } + var late: TokenTradeMode + } + """ + self.assertEqual(self.names(body), {"PersistentTxo", "TokenTradeMode"}) + + def test_a_type_declared_in_the_models_own_extension_is_not_missing(self): + body = """ + final class PersistentThing { + var kind: Kind + } + """.strip("\n").splitlines() + bodies = {("DashSchemaV1", "PersistentThing"): (body, {"Kind"})} + frozen = {"DashSchemaV1": {"PersistentThing"}} + self.assertEqual(gen.closure_problems(frozen, bodies, {"DashSchemaV1": set()}), []) + bodies = {("DashSchemaV1", "PersistentThing"): (body, set())} + [problem] = gen.closure_problems(frozen, bodies, {"DashSchemaV1": set()}) + self.assertIn("stores Kind", problem) + + def test_enum_payloads_count_but_raw_values_do_not(self): + body = """ + enum Distribution: String, Codable { + case none = "None" + case perpetual(TokenPerpetualDistribution) + case scheduled(events: [DistributionEvent]) + } + """ + self.assertEqual( + self.names(body), {"TokenPerpetualDistribution", "DistributionEvent"} + ) + + +if __name__ == "__main__": + unittest.main() From 43226c9fc35ec09b1039f6255d77b13b0d262e0e Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 9 Sep 2026 16:18:12 +0700 Subject: [PATCH 04/12] ci(swift-sdk): check the frozen schema models on every Swift change Nothing in CI invoked `freeze_schema_models.py --check`, so the drift protection the generated freeze exists for was never enforced. A new `swift-sdk-frozen-schema` job, gated on the existing `swift-sdk-changed` filter, runs the check and the generator's tests on an Ubuntu runner. The check rebuilds every frozen file from the commits named in FREEZES, so it fails on a shallow checkout ("invalid object name"); the job checks out with full history, as the `changes` job already does. Making the check history-free would mean trusting the committed files, which is the very thing it exists to distrust, and fetching the named commits by SHA would duplicate the table in the workflow. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/tests.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bfbb7183c65..6ed43dccdb7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -477,6 +477,26 @@ jobs: secrets: inherit uses: ./.github/workflows/swift-sdk-build.yml + swift-sdk-frozen-schema: + name: Swift SDK frozen schema check + needs: changes + if: ${{ needs.changes.outputs.swift-sdk-changed == 'true' }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Check out repo + uses: actions/checkout@v4 + with: + # The generator rebuilds every frozen SwiftData model from the + # commits its FREEZES table names, so the check needs history. + fetch-depth: 0 + + - name: Check the frozen SwiftData models match FREEZES + run: python3 packages/swift-sdk/scripts/freeze_schema_models.py --check + + - name: Test the freeze generator + run: python3 -m unittest discover -s packages/swift-sdk/scripts -p 'test_*.py' -v + js-packages: name: JS packages needs: From a48b8a98cab93d405da0c814e369a8dff0efb83d Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 9 Sep 2026 17:31:33 +0700 Subject: [PATCH 05/12] refactor(swift-sdk): make the hash test the authority on freeze completeness Remove the static closure and registration validation from freeze_schema_models.py. A text scan of Swift source cannot decide which references feed a SwiftData entity hash: it misses syntax it does not understand (multiline declarations, nested types declared in extensions) and it cannot tell that a struct stored directly on a model is hashed while an array of structs nested inside one is not. Each gap it misses is a silent Cocoa 134504 in the field, and each false flag blocks a legitimate freeze. `--check` now claims only what it proves: the committed frozen files are the generator's byte-for-byte output. The generator's tests cover that comparison (hand edit, missing file, stale file) and the brace counting. The authority is `DashModelMigrationTests.testFrozenVersionsBuiltAfterTheLiveSchemaHashLikeTheStoresTheyShipped`, which builds each released version after the live schema and compares per-entity hashes, entity membership and the checksum with a store the shipping build wrote. Probed with the partial-freeze generator against that test: omitting the `PersistentTxo` relationship target fails all three fixtures today (the live TXO already gained a column); omitting the stored `ChangeControlRules` or `TokenPreProgrammedDistribution` passes while the live struct is unchanged and fails on the change that would break the store, while the complete freeze passes that same change; returning `[]` from every released version's `models` fails on membership; omitting `DistributionEvent` never fails, because SwiftData does not hash it (an array of structs inside a stored struct), so it was never a risk. The residual limit is named in the test's doc and the container's: the test guards only versions with a committed fixture store (V1, V2, V3), so committing a fixture written by the shipping build is a required step of cutting a schema version. Co-Authored-By: Claude Fable 5.1 --- .../Persistence/DashModelContainer.swift | 6 +- .../DashModelMigrationTests.swift | 24 ++ .../swift-sdk/scripts/freeze_schema_models.py | 357 +++--------------- .../scripts/test_freeze_schema_models.py | 341 +++-------------- 4 files changed, 137 insertions(+), 591 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift index 8f9760f0e20..12458d2836c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift @@ -90,7 +90,11 @@ public enum DashModelContainer { /// so it moves whenever a model gains a property — which is exactly why /// the released versions must not. When the next property lands: freeze /// every model here into the version being retired - /// (`scripts/freeze_schema_models.py`), add a version, add a stage. + /// (`scripts/freeze_schema_models.py`), add a version, add a stage, and + /// commit a store written by the build that shipped the retired version + /// under the test fixtures. `DashModelMigrationTests` proves a freeze + /// complete only against such a store; until it lands the new freeze is + /// unguarded. public static var modelTypes: [any PersistentModel.Type] { [ PersistentIdentity.self, diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift index 2da7642c89c..7114b7249a8 100644 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift @@ -193,6 +193,30 @@ final class DashModelMigrationTests: XCTestCase { /// from a live `PersistentAccount.wallet` is rebound to the live /// wallet's shape the moment the live schema is built first, and the /// released checksum moves with it. + /// + /// This test is THE authority on whether a freeze is complete. The + /// generator's `--check` (`scripts/freeze_schema_models.py`) only proves + /// the committed frozen files are the generator's byte-for-byte output; + /// it does not, and must not try to, decide whether the `FREEZES` table + /// covers every relationship target and stored value type. A static + /// scan of Swift source cannot: it misses whatever syntax it does not + /// understand, and it flags references SwiftData does not hash at all + /// (a struct stored directly on a model is part of the entity hash; an + /// array of structs nested inside it is not), so it fails silently in + /// both directions. Only + /// building the schema and reading the hash SwiftData computes, against + /// a store a shipping build wrote, answers the question, and that is + /// what this does: an omitted relationship target fails here as soon as + /// the live target has changed shape, an omitted stored value type + /// fails here on the change that would have broken the store, and a + /// version registering the wrong entity set fails on membership. + /// + /// Its reach is exactly the fixtures: it guards a version only once a + /// store written by a build that shipped that version is committed + /// under `Fixtures/SchemaStores/` and listed in `fixtures`. V1, V2 and + /// V3 are covered. A newly cut version's freeze is unguarded until its + /// fixture lands, so committing that fixture is a required step of + /// cutting a schema version, not an optional one. func testFrozenVersionsBuiltAfterTheLiveSchemaHashLikeTheStoresTheyShipped() throws { for fixture in Self.fixtures { let (directory, url) = try copyFixture(fixture) diff --git a/packages/swift-sdk/scripts/freeze_schema_models.py b/packages/swift-sdk/scripts/freeze_schema_models.py index b601a800f52..94b5c7f273d 100755 --- a/packages/swift-sdk/scripts/freeze_schema_models.py +++ b/packages/swift-sdk/scripts/freeze_schema_models.py @@ -46,29 +46,32 @@ migration stage, and rerunning this script. Never edit an existing row; a released checksum cannot move. -The table is not trusted to be complete. Before anything is generated or -checked, the freeze is validated as a closed graph, and an incomplete one -is refused with the missing names rather than emitted partially: - - - every type a frozen model or frozen value type stores (a relationship - target, an inline Codable struct or enum) must itself be frozen under - the same schema, transitively; a bare name in a nested class body that - has no nested sibling resolves to the live type, which is exactly the - silent drift the freeze exists to prevent; - - every `DashSchemaVN.X` that `DashModelContainer.swift` registers must be - produced by the table, every frozen model must be registered, and no - released version may register a live model type. +Nothing here checks that the table is COMPLETE. That is deliberate. A +frozen model whose relationship target or stored value type is missing +from the table binds that bare name to the live type, and the released +checksum then moves with the live type's next change; but whether a given +Swift reference feeds the entity hash is decided by SwiftData (a struct +stored directly on a model does, an array of structs nested inside one does +not), and a text scan of Swift source cannot know that, nor keep up with +optionals, generics, extensions, nested types and enum payloads. Every +reference such a scan misses is a silent failure in the field, and every +one it wrongly flags is a false alarm. The authority for completeness is `DashModelMigrationTests.testFrozenVersionsBuiltAfterTheLiveSchemaHashLikeTheStoresTheyShipped`, +which builds each released version after the live schema and compares the +hashes SwiftData computes against a store the shipping build wrote. Do not +add static validation here; extend that test (and its fixtures) instead. Usage, from anywhere inside the repository: scripts/freeze_schema_models.py # regenerate every frozen file scripts/freeze_schema_models.py --check # exit 1 if any file would change -`--check` is what a reviewer or CI runs (the `swift-sdk-frozen-schema` job -in `.github/workflows/tests.yml`): the frozen files are a pure function of -`FREEZES` and the repository history, so a clean check proves nobody edited -a frozen copy by hand. It needs the commits named in `FREEZES` to be -present locally, so CI runs it on a full-history checkout. +`--check` is a regeneration check and nothing more: the frozen files are a +pure function of `FREEZES` and the repository history, so a clean check +proves that the committed files are exactly this generator's output, byte +for byte, and that no one edited a frozen copy by hand. It says nothing +about whether the freeze is complete. CI runs it (the +`swift-sdk-frozen-schema` job in `.github/workflows/tests.yml`) on a +full-history checkout, because it needs the commits named in `FREEZES`. The generator's own tests, from the repository root: @@ -85,36 +88,6 @@ MODELS_DIR = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models" OUT_DIR = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas" TOKEN_TYPES_FILE = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Types/TokenTypes.swift" -CONTAINER_FILE = "packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift" - -# Types SwiftData stores natively: they carry no shape of their own, so a -# stored property of one of these adds nothing that has to be frozen. -BUILTIN_TYPES = { - "Bool", - "Data", - "Date", - "Decimal", - "Double", - "Float", - "Int", - "Int8", - "Int16", - "Int32", - "Int64", - "String", - "UInt", - "UInt8", - "UInt16", - "UInt32", - "UInt64", - "URL", - "UUID", - "Array", - "Dictionary", - "Optional", - "Set", -} - # The value types `PersistentToken` stores inline, plus the enums their # initializers reference. TOKEN_VALUE_TYPES = [ @@ -314,182 +287,6 @@ def extract_extensions(source, model): return bodies -ATTRIBUTE = r"@\w+(\((?:[^()]|\([^()]*\))*\))?\s*" -MODIFIER = ( - r"(?:(?:private|fileprivate|internal|public|open|package)(?:\(set\))?" - r"|static|lazy|weak|unowned|final)\s+" -) -STORED_DECLARATION = re.compile( - rf"^\s*(?P(?:{ATTRIBUTE})*)(?P(?:{MODIFIER})*)(var|let)\s+\w+\s*" - r"(?::\s*(?P[^={]+))?(?:=\s*(?P[^{]*))?" -) -ENUM_CASE = re.compile(r"^\s*(indirect\s+)?case\s+(?P.+)") -NESTED_TYPE = re.compile( - rf"^\s*(?:{MODIFIER})*(?:indirect\s+)?(struct|enum|class)\s+(?P\w+)" -) - - -def type_names(expression): - """Capitalised identifiers a type expression or enum payload mentions. - - String literals are dropped (an enum raw value is not a type) and so is - everything after a dot: `Foo.Bar` belongs to `Foo`, and the nested type - is copied along with it. - """ - expression = re.sub(r'"[^"]*"', "", expression) - expression = re.sub(r"\.\w+", "", expression) - return set(re.findall(r"\b[A-Z]\w*", expression)) - - -def stored_type_names(block): - """Type names the stored declarations of one comment-stripped type body mention. - - A stored property (`var` / `let` with no getter body and no `@Transient`) - or an enum case payload is what SwiftData hashes, so those are the - references that must resolve to frozen copies. Types declared nested in - the body are copied with it, so their names are not returned, but their - own stored declarations are walked: a nested struct's field binds a - name the same way the model's does. Computed properties, initializers, - methods, static members and the raw-value clause do not affect the - entity, and a builtin has no shape to freeze, so none of those are - returned. A property whose type is inferred from its initializer - contributes the initializer's type names instead. - """ - names, nested = set(), set() - depth = 0 - attributes = "" - for i, raw in enumerate(block): - line = code_only(raw) - if depth == 1: - declared = NESTED_TYPE.match(line) - if declared: - nested.add(declared.group("name")) - names |= stored_type_names(block[i : block_end(block, i) + 1]) - if line.strip().startswith("@") and not re.search(r"\b(var|let)\b", line): - attributes += line - else: - following = next((code_only(l) for l in block[i + 1 :] if l.strip()), "") - if re.search(r"\b(var|let)\s+\w+\s*:\s*$", line): - line += " " + following.strip() - stored = STORED_DECLARATION.match(line) - if stored and "static" not in stored.group("modifiers"): - observed = re.search(r"\b(didSet|willSet)\b", line + " " + following) - computed = "{" in line and not observed - transient = "@Transient" in attributes + stored.group("attributes") - if not computed and not transient: - names |= type_names( - stored.group("type") or stored.group("initializer") or "" - ) - case = ENUM_CASE.match(line) - if case: - names |= type_names(case.group("payload")) - attributes = "" - depth += braces(raw) - return names - nested - BUILTIN_TYPES - - -def model_names_at(root, commit): - """Every `@Model` class under MODELS_DIR as of `commit`.""" - grep = subprocess.run( - ["git", "grep", "-l", "@Model", commit, "--", MODELS_DIR], - text=True, - encoding="utf-8", - cwd=root, - capture_output=True, - ) - if grep.returncode > 1: - raise SystemExit(f"git grep at {commit} failed: {grep.stderr.strip()}") - return { - os.path.basename(line.split(":", 1)[1])[: -len(".swift")] - for line in grep.stdout.splitlines() - } - - -def closure_problems(frozen, bodies, models_at): - """Why the freeze is not closed under "stores", as human-readable lines. - - `frozen` maps a schema to every name frozen under it, `bodies` maps - `(schema, name)` to the comment-stripped body of that frozen type plus - the names of the types its own extensions declare (those are copied - with it), and `models_at` maps a schema to the live model names at the - commits its rows are taken from. A stored reference must resolve to a - frozen sibling; a bare name with no sibling binds to the live type - instead, and its next change would move the released checksum. - """ - problems = [] - for (schema, owner), (body, declared) in sorted(bodies.items()): - for name in sorted(stored_type_names(body) - frozen[schema] - declared): - what = "a model" if name in models_at[schema] else "a value type" - problems.append( - f"{schema}.{owner} stores {name}, which is not frozen under {schema}: " - f"add it to a {schema} row of FREEZES as {what}" - ) - return problems - - -def registration_problems(container_lines, frozen_models, live_models): - """Why DashModelContainer.swift and FREEZES disagree, as human-readable lines. - - Every `DashSchemaVN.X` the container names must be produced by the - table, every frozen model must be named somewhere (else it is a copy no - version registers), a live model type may appear only inside the live - list `modelTypes`, and only the newest `DashSchemaVN` may use that - list: a released version that registered a live shape would hash it - into its checksum. - - This is a presence check, not per-version membership: a version that - registered the wrong frozen copy (say V2 taking V3's asset lock) passes - here and is caught by `DashModelMigrationTests` opening real stores. - """ - # Comments are blanked line by line rather than dropped so that a - # reported line number is the file's own. - lines = [code_only(line) for line in container_lines] - # The live list is found by its exact declaration; a reformat that - # hides it makes the check refuse rather than pass. - live_list = range(0) - versions = {} - for i, line in enumerate(lines): - if re.match(r"^\s*(public )?static var modelTypes: \[any PersistentModel\.Type\] \{", line): - live_list = range(i, block_end(lines, i) + 1) - version = re.match(r"^\s*(public )?enum DashSchemaV(\d+)\b", line) - if version: - versions[int(version.group(2))] = range(i, block_end(lines, i) + 1) - problems = [] - if not live_list: - problems.append(f"{CONTAINER_FILE}: the live model list `modelTypes` was not found") - # Only the newest version may be the live one; an older version that - # aliased `modelTypes` would register every live shape at once. - live_version = versions[max(versions)] if versions else range(0) - registered = set() - for i, line in enumerate(lines): - for schema, name in re.findall(r"\b(DashSchemaV\d+)\.([A-Z]\w*)\b", line): - registered.add((schema, name)) - if i in live_list: - continue - aliased = versions and re.search(r"\bmodelTypes\b", line) - if aliased and i not in live_version: - problems.append( - f"{CONTAINER_FILE} line {i + 1} uses the live model list `modelTypes` " - f"outside DashSchemaV{max(versions)}; a released version may only " - "register frozen copies" - ) - for name in re.findall(r"(? Result { fatalError() } - } - """ - self.assertEqual(self.names(body), set()) - - def test_access_modifiers_and_inferred_types_still_count(self): - body = """ - @Model - final class PersistentThing { - private var rules: ChangeControlRules? - private(set) var localization: TokenLocalization? - var tradeMode = TokenTradeMode.notTradeable - let events = [DistributionEvent]() - var stamp = Date() - static let shared: Registry = Registry() - @Attribute(.transformable(by: ColorTransformer.self)) var color: ColorBox - var observed: Counter { - didSet { print("changed") } - } - } - """ self.assertEqual( - self.names(body), - { - "ChangeControlRules", - "TokenLocalization", - "TokenTradeMode", - "DistributionEvent", - "ColorBox", - "Counter", - }, + gen.check_problems(self.scratch, self.files), + [f"missing: {missing}", f"stale: {stale}"], ) - def test_fields_of_a_nested_type_are_walked(self): - body = """ - @Model - final class PersistentThing { - var meta: Meta? - struct Meta: Codable { - var rules: ChangeControlRules - var kind: Kind - enum Kind: String, Codable { - case a(DistributionEvent) - } - } - } - """ - self.assertEqual(self.names(body), {"ChangeControlRules", "DistributionEvent"}) - - def test_transient_comments_strings_and_split_declarations(self): - body = """ - @Model - final class PersistentThing { - @Transient var cache: LiveCache? = nil - var id: Data // Base58 identity id, see IdentityEntry - @Relationship(deleteRule: .cascade) - var outputs: - [PersistentTxo] = [] - init() { let brace = "{"; _ = brace } - var late: TokenTradeMode - } - """ - self.assertEqual(self.names(body), {"PersistentTxo", "TokenTradeMode"}) - def test_a_type_declared_in_the_models_own_extension_is_not_missing(self): - body = """ - final class PersistentThing { - var kind: Kind - } - """.strip("\n").splitlines() - bodies = {("DashSchemaV1", "PersistentThing"): (body, {"Kind"})} - frozen = {"DashSchemaV1": {"PersistentThing"}} - self.assertEqual(gen.closure_problems(frozen, bodies, {"DashSchemaV1": set()}), []) - bodies = {("DashSchemaV1", "PersistentThing"): (body, set())} - [problem] = gen.closure_problems(frozen, bodies, {"DashSchemaV1": set()}) - self.assertIn("stores Kind", problem) - - def test_enum_payloads_count_but_raw_values_do_not(self): - body = """ - enum Distribution: String, Codable { - case none = "None" - case perpetual(TokenPerpetualDistribution) - case scheduled(events: [DistributionEvent]) - } - """ - self.assertEqual( - self.names(body), {"TokenPerpetualDistribution", "DistributionEvent"} - ) +class BlockEndTests(unittest.TestCase): + """A brace that is not code must not end the copied block early.""" + + def test_braces_in_strings_and_comments_do_not_count(self): + lines = [ + "final class PersistentThing {", + ' init() { let brace = "{"; _ = brace }', + " // a } in a comment", + " var late: Int", + "}", + "extension PersistentThing {}", + ] + self.assertEqual(gen.block_end(lines, 0), 4) + + def test_a_block_comment_is_refused_rather_than_misread(self): + lines = ["final class PersistentThing {", " /* { */", "}"] + with self.assertRaises(SystemExit): + gen.block_end(lines, 0) if __name__ == "__main__": From 30c205d4de55582dd46c8c0b9b27ea898cf20381 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 9 Sep 2026 17:31:39 +0700 Subject: [PATCH 06/12] ci: run the Swift SDK jobs when tests.yml itself changes The `swift-sdk-changed` filter gates both the Swift build and the frozen schema check, but did not list the workflow that defines them, so a change to the job definitions alone never exercised them. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6ed43dccdb7..86d9675f743 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -307,6 +307,7 @@ jobs: filters: | swift-sdk-changed: - .github/workflows/swift-sdk-build.yml + - .github/workflows/tests.yml - packages/swift-sdk/** - packages/dapi-grpc/** - packages/dashpay-contract/** From 3af9de824335f97d46e1cc9937795b0a9ac4fb1a Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 9 Sep 2026 19:23:28 +0700 Subject: [PATCH 07/12] test(swift-sdk): enforce fixture coverage and check the indexes the hash cannot see The hash test guards a retired schema version only through its fixture store, and the fixture list was maintained by hand: a new version and migration stage added without a fixture left the suite green and the new freeze unguarded. The test now asserts, first, that `fixtures` lists exactly the retired versions of `DashMigrationPlan.schemas`, in order and once each, so cutting a version fails the suite until the store written by the build that shipped it is committed. Probed by adding a DashSchemaV5 and stage in a scratch copy: the assertion fails with ["1.0.0", "2.0.0", "3.0.0"] versus ["1.0.0", "2.0.0", "3.0.0", "4.0.0"]. Core Data leaves `#Index` out of entity version hashes, so the hash test could not see index drift while its doc claimed `#Index` among the inputs. The claim is narrowed in the test, the container and the generator docs, and a sibling test reads `sqlite_master` to check what the hash cannot: each fixture, as written, has exactly the indexes a store built fresh from its frozen version has, and after migrating through the container factory it has every index a fresh live store has. The second is a superset check because a store rebuilt by a migration can keep an index from an earlier layout (the V1 fixture keeps one join-table index). Probed: drifting the frozen V3 asset lock's `#Index` to another column fails the index test on the V3 fixture while the hash test stays green; a toy two-version plan whose only change is an index migrates with an unchanged checksum and without the index a fresh store has, and the superset check reports exactly that index. On the committed tree all nine migration tests pass. Co-Authored-By: Claude Fable 5.1 --- .../Persistence/DashModelContainer.swift | 4 +- .../DashModelMigrationTests.swift | 139 +++++++++++++++--- .../swift-sdk/scripts/freeze_schema_models.py | 18 ++- 3 files changed, 133 insertions(+), 28 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift index 12458d2836c..f393ed93372 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift @@ -93,8 +93,8 @@ public enum DashModelContainer { /// (`scripts/freeze_schema_models.py`), add a version, add a stage, and /// commit a store written by the build that shipped the retired version /// under the test fixtures. `DashModelMigrationTests` proves a freeze - /// complete only against such a store; until it lands the new freeze is - /// unguarded. + /// complete (in what the entity hash covers, plus its indexes) only + /// against such a store, and fails until every retired version has one. public static var modelTypes: [any PersistentModel.Type] { [ PersistentIdentity.self, diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift index 7114b7249a8..b5f642d9fb3 100644 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift @@ -1,5 +1,6 @@ import CoreData import Foundation +import SQLite3 import SwiftData import XCTest @@ -194,30 +195,44 @@ final class DashModelMigrationTests: XCTestCase { /// wallet's shape the moment the live schema is built first, and the /// released checksum moves with it. /// - /// This test is THE authority on whether a freeze is complete. The - /// generator's `--check` (`scripts/freeze_schema_models.py`) only proves - /// the committed frozen files are the generator's byte-for-byte output; - /// it does not, and must not try to, decide whether the `FREEZES` table - /// covers every relationship target and stored value type. A static - /// scan of Swift source cannot: it misses whatever syntax it does not - /// understand, and it flags references SwiftData does not hash at all - /// (a struct stored directly on a model is part of the entity hash; an - /// array of structs nested inside it is not), so it fails silently in - /// both directions. Only - /// building the schema and reading the hash SwiftData computes, against - /// a store a shipping build wrote, answers the question, and that is - /// what this does: an omitted relationship target fails here as soon as - /// the live target has changed shape, an omitted stored value type - /// fails here on the change that would have broken the store, and a - /// version registering the wrong entity set fails on membership. + /// This test is THE authority on whether a freeze is complete in every + /// respect the entity hash covers: stored properties, their types, + /// optionality and defaults, relationships and their inverses, and + /// `#Unique` constraints. The generator's `--check` + /// (`scripts/freeze_schema_models.py`) only proves the committed frozen + /// files are the generator's byte-for-byte output; it does not, and + /// must not try to, decide whether the `FREEZES` table covers every + /// relationship target and stored value type. A static scan of Swift + /// source cannot: it misses whatever syntax it does not understand, and + /// it flags references SwiftData does not hash at all (a struct stored + /// directly on a model is part of the entity hash; an array of structs + /// nested inside it is not), so it fails silently in both directions. + /// Only building the schema and reading the hash SwiftData computes, + /// against a store a shipping build wrote, answers the question, and + /// that is what this does: an omitted relationship target fails here as + /// soon as the live target has changed shape, an omitted stored value + /// type fails here on the change that would have broken the store, and + /// a version registering the wrong entity set fails on membership. + /// + /// What the hash does NOT cover is `#Index`: Core Data leaves indexes + /// out of entity version hashes, so an index that drifted in a frozen + /// copy, or one a migration never created, passes here. That is what + /// `testFixturesAndMigratedStoresCarryTheIndexesFreshStoresHave` is for. /// /// Its reach is exactly the fixtures: it guards a version only once a /// store written by a build that shipped that version is committed - /// under `Fixtures/SchemaStores/` and listed in `fixtures`. V1, V2 and - /// V3 are covered. A newly cut version's freeze is unguarded until its - /// fixture lands, so committing that fixture is a required step of - /// cutting a schema version, not an optional one. + /// under `Fixtures/SchemaStores/` and listed in `fixtures`. So the first + /// thing checked is that `fixtures` lists every retired version in the + /// migration plan, once each: cutting a new schema version fails this + /// test until the fixture written by the build that shipped the retired + /// version is committed. func testFrozenVersionsBuiltAfterTheLiveSchemaHashLikeTheStoresTheyShipped() throws { + XCTAssertEqual( + Self.fixtures.map { Self.describe($0.version.versionIdentifier) }, + DashMigrationPlan.schemas.dropLast().map { Self.describe($0.versionIdentifier) }, + "every retired schema version needs a fixture store written by the build that " + + "shipped it, listed once in `fixtures`; without one its freeze is unguarded") + for fixture in Self.fixtures { let (directory, url) = try copyFixture(fixture) defer { try? FileManager.default.removeItem(at: directory) } @@ -243,6 +258,90 @@ final class DashModelMigrationTests: XCTestCase { } } + private static func describe(_ version: Schema.Version) -> String { + "\(version.major).\(version.minor).\(version.patch)" + } + + /// The SQLite indexes of a store, one line per index: table, name and + /// the statement that created it. Auto-indexes SQLite makes for its + /// own constraints have no statement and are listed as such. + private static func indexes(at url: URL) throws -> Set { + var database: OpaquePointer? + guard sqlite3_open_v2(url.path, &database, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else { + sqlite3_close(database) + struct StoreUnreadable: Error {} + XCTFail("\(url.lastPathComponent): could not be opened as SQLite") + throw StoreUnreadable() + } + defer { sqlite3_close(database) } + var statement: OpaquePointer? + let query = "SELECT tbl_name, name, sql FROM sqlite_master WHERE type = 'index'" + XCTAssertEqual(sqlite3_prepare_v2(database, query, -1, &statement, nil), SQLITE_OK) + defer { sqlite3_finalize(statement) } + var rows = Set() + while sqlite3_step(statement) == SQLITE_ROW { + let table = String(cString: sqlite3_column_text(statement, 0)) + let name = String(cString: sqlite3_column_text(statement, 1)) + let sql = sqlite3_column_text(statement, 2).map { String(cString: $0) } ?? "(auto)" + rows.insert("\(table) \(name): \(sql)") + } + return rows + } + + /// The check the entity hash cannot give. Core Data leaves `#Index` out + /// of version hashes, so the test above stays green when a frozen + /// copy's index differs from what shipped, and a lightweight migration + /// whose only change is an index can complete without creating it. + /// Both show up in SQLite, so that is where they are checked: + /// + /// - a fixture, as written, has exactly the indexes a store built + /// fresh from its frozen version has (the frozen `#Index` is what + /// shipped); + /// - after migrating through `DashModelContainer.create`, a fixture + /// has every index a store built fresh at the live version has (the + /// migration created what the live model declares). + /// + /// The second is a superset check, not equality: a store rebuilt by a + /// migration can keep an index from an earlier layout, which costs a + /// little space and nothing else. + @MainActor + func testFixturesAndMigratedStoresCarryTheIndexesFreshStoresHave() throws { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory( + at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + + let freshLive = directory.appendingPathComponent("live.store") + _ = try DashModelContainer.create(url: freshLive) + let liveIndexes = try Self.indexes(at: freshLive) + XCTAssertFalse(liveIndexes.isEmpty) + + for fixture in Self.fixtures { + let (fixtureDirectory, url) = try copyFixture(fixture) + defer { try? FileManager.default.removeItem(at: fixtureDirectory) } + + let freshAtVersion = fixtureDirectory.appendingPathComponent("fresh.store") + let schema = Schema(versionedSchema: fixture.version) + _ = try ModelContainer( + for: schema, + configurations: [ + ModelConfiguration( + "DashSchemaIndexes", schema: schema, url: freshAtVersion, + allowsSave: true, cloudKitDatabase: .none) + ]) + XCTAssertEqual( + try Self.indexes(at: url).symmetricDifference(try Self.indexes(at: freshAtVersion)), + [], + "\(fixture.name): the frozen version's indexes differ from what shipped") + + _ = try DashModelContainer.create(url: url) + XCTAssertEqual( + liveIndexes.subtracting(try Self.indexes(at: url)), [], + "\(fixture.name): indexes a fresh live store has that the migration did not create") + } + } + @MainActor func testV1StoreMigratesToV2AndAcceptsTrackedMasternodes() throws { let directory = FileManager.default.temporaryDirectory diff --git a/packages/swift-sdk/scripts/freeze_schema_models.py b/packages/swift-sdk/scripts/freeze_schema_models.py index 94b5c7f273d..cf0042064aa 100755 --- a/packages/swift-sdk/scripts/freeze_schema_models.py +++ b/packages/swift-sdk/scripts/freeze_schema_models.py @@ -21,9 +21,10 @@ copied; doc comments, `public` modifiers and top-level enums are dropped. Extensions add no stored properties (so they are not part of the entity) but the class body may call into them. The stored properties, their -optionality and defaults, -`@Attribute`, `@Relationship`, `#Index` and `#Unique` are what the checksum -hashes, and they are copied verbatim. +optionality and defaults, `@Attribute`, `@Relationship` and `#Unique` are +what the checksum hashes, and they are copied verbatim. `#Index` is copied +verbatim too but is NOT part of the hash (Core Data leaves indexes out of +entity version hashes), which is why index drift needs its own check. Value types a model stores inline (Codable structs and raw enums SwiftData expands into composite attributes, such as `ChangeControlRules` on @@ -55,10 +56,15 @@ not), and a text scan of Swift source cannot know that, nor keep up with optionals, generics, extensions, nested types and enum payloads. Every reference such a scan misses is a silent failure in the field, and every -one it wrongly flags is a false alarm. The authority for completeness is `DashModelMigrationTests.testFrozenVersionsBuiltAfterTheLiveSchemaHashLikeTheStoresTheyShipped`, +one it wrongly flags is a false alarm. The authority for +hash-relevant completeness (properties, relationships, `#Unique`) is +`DashModelMigrationTests.testFrozenVersionsBuiltAfterTheLiveSchemaHashLikeTheStoresTheyShipped`, which builds each released version after the live schema and compares the -hashes SwiftData computes against a store the shipping build wrote. Do not -add static validation here; extend that test (and its fixtures) instead. +hashes SwiftData computes against a store the shipping build wrote; its +sibling `testFixturesAndMigratedStoresCarryTheIndexesFreshStoresHave` +covers `#Index`, which the hash cannot see, by comparing SQLite indexes. +Do not add static validation here; extend those tests (and their +fixtures) instead. Usage, from anywhere inside the repository: From 9089205178a2939ff47068540ce03eebafc887d3 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 9 Sep 2026 19:23:30 +0700 Subject: [PATCH 08/12] test(swift-sdk): name the generator tests by what they should do Co-Authored-By: Claude Fable 5.1 --- .../swift-sdk/scripts/test_freeze_schema_models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/swift-sdk/scripts/test_freeze_schema_models.py b/packages/swift-sdk/scripts/test_freeze_schema_models.py index 975de4ed643..260c8670a05 100644 --- a/packages/swift-sdk/scripts/test_freeze_schema_models.py +++ b/packages/swift-sdk/scripts/test_freeze_schema_models.py @@ -40,17 +40,17 @@ def setUp(self): os.path.join(ROOT, gen.OUT_DIR), os.path.join(self.scratch, gen.OUT_DIR) ) - def test_the_committed_files_are_the_generators_output(self): + def test_should_find_the_committed_files_are_the_generators_output(self): self.assertEqual(len(self.files), 37) self.assertEqual(gen.check_problems(ROOT, self.files), []) - def test_a_hand_edit_to_a_frozen_file_is_reported(self): + def test_should_report_a_hand_edit_to_a_frozen_file(self): path = f"{gen.OUT_DIR}/DashSchemaV1+PersistentWallet.swift" with open(os.path.join(self.scratch, path), "a", encoding="utf-8") as f: f.write("// edited by hand\n") self.assertEqual(gen.check_problems(self.scratch, self.files), [f"differs: {path}"]) - def test_a_missing_and_a_stale_file_are_reported(self): + def test_should_report_a_missing_and_a_stale_file(self): missing = f"{gen.OUT_DIR}/DashSchemaV3+PersistentAssetLock.swift" stale = f"{gen.OUT_DIR}/DashSchemaV9+PersistentGhost.swift" os.rename( @@ -65,7 +65,7 @@ def test_a_missing_and_a_stale_file_are_reported(self): class BlockEndTests(unittest.TestCase): """A brace that is not code must not end the copied block early.""" - def test_braces_in_strings_and_comments_do_not_count(self): + def test_should_not_count_braces_in_strings_and_comments(self): lines = [ "final class PersistentThing {", ' init() { let brace = "{"; _ = brace }', @@ -76,7 +76,7 @@ def test_braces_in_strings_and_comments_do_not_count(self): ] self.assertEqual(gen.block_end(lines, 0), 4) - def test_a_block_comment_is_refused_rather_than_misread(self): + def test_should_refuse_a_block_comment_rather_than_misread_it(self): lines = ["final class PersistentThing {", " /* { */", "}"] with self.assertRaises(SystemExit): gen.block_end(lines, 0) From 66feb5589e78c3568421c1c0b5e68397013139b3 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 9 Sep 2026 21:19:22 +0700 Subject: [PATCH 09/12] test(swift-sdk): pin the migration plan to an append-only shipped-version list The fixture-coverage assertion derived its expectation from the migration plan it was checking, so it caught an appended version but not a replaced one: a plan of V1, V2, V3, V5 still had the three fixtures for everything but its last entry, while V4 stores in the field would match no version. The expectation is now a literal, append-only list of every version that shipped, and the plan must equal it exactly; a fixture is required for every entry but the last. Removing, reordering or replacing a shipped version fails, and so does appending one without appending to the list. The container's live schema was declared independently of the plan, and SwiftData accepts a plan whose tail is newer than the schema it is asked to migrate to, so a version appended to the plan but not made the container's schema left the app on the old shape while every test targeted the new one. The live schema must now be the plan's last version. Both proven in scratch copies of the container: V4 replaced by V5 fails the plan guard with [1.0.0, 2.0.0, 3.0.0, 5.0.0] versus the shipped list; V5 appended with its stage while the container stays on V4 fails the live-schema guard with 4.0.0 versus 5.0.0. The SQLite index listing now requires SQLITE_DONE as its terminal step result, so a listing cut short by a busy, corrupt, I/O or memory error is refused instead of weakening the superset check. The note on that check no longer calls a retained index free: it is not a compatibility problem, but it takes storage and is maintained on every write, and the check does not look for it. Co-Authored-By: Claude Fable 5.1 --- .../DashModelMigrationTests.swift | 68 ++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift index b5f642d9fb3..8f56afee398 100644 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift @@ -59,6 +59,15 @@ final class DashModelMigrationTests: XCTestCase { hasTrackedMasternode: true, assetLockRecipientIsExternal: true), ] + /// Every schema version that has ever shipped, oldest first, as + /// `major.minor.patch`. APPEND-ONLY: a version that shipped wrote stores + /// that exist in the field, so it can never be removed from, reordered + /// in, or replaced in the migration plan, and the plan is checked + /// against this list rather than the other way round. Adding a version + /// to the plan is shipping it: append it here in the same change, and + /// give the version before it a fixture store in `fixtures`. + private static let shippedVersions = ["1.0.0", "2.0.0", "3.0.0", "4.0.0"] + private static let fixtureWalletId = Data(repeating: 0x31, count: 32) private static let fixtureSpendTxid = Data(repeating: 0x32, count: 32) private static let fixtureFundingTxid = Data(repeating: 0x34, count: 32) @@ -222,14 +231,16 @@ final class DashModelMigrationTests: XCTestCase { /// Its reach is exactly the fixtures: it guards a version only once a /// store written by a build that shipped that version is committed /// under `Fixtures/SchemaStores/` and listed in `fixtures`. So the first - /// thing checked is that `fixtures` lists every retired version in the - /// migration plan, once each: cutting a new schema version fails this + /// thing checked is that `fixtures` lists every retired version in + /// `shippedVersions`, once each: cutting a new schema version fails this /// test until the fixture written by the build that shipped the retired - /// version is committed. + /// version is committed. The expectation comes from the append-only + /// list, not from the migration plan, so a plan that dropped a version + /// cannot shrink it (`testShippedSchemaVersionsStayInTheMigrationPlan`). func testFrozenVersionsBuiltAfterTheLiveSchemaHashLikeTheStoresTheyShipped() throws { XCTAssertEqual( Self.fixtures.map { Self.describe($0.version.versionIdentifier) }, - DashMigrationPlan.schemas.dropLast().map { Self.describe($0.versionIdentifier) }, + Array(Self.shippedVersions.dropLast()), "every retired schema version needs a fixture store written by the build that " + "shipped it, listed once in `fixtures`; without one its freeze is unguarded") @@ -262,6 +273,36 @@ final class DashModelMigrationTests: XCTestCase { "\(version.major).\(version.minor).\(version.patch)" } + /// The migration plan must list exactly the versions that ever shipped, + /// in the order they shipped, with nothing removed, reordered or + /// replaced: a store written by any of them is still in the field and + /// must be recognised. The expectation is the append-only + /// `shippedVersions`, never the plan itself, so editing the plan cannot + /// move the goalposts; a version can only enter the plan by being + /// appended to that list in the same change. + func testShippedSchemaVersionsStayInTheMigrationPlan() { + XCTAssertEqual( + DashMigrationPlan.schemas.map { Self.describe($0.versionIdentifier) }, + Self.shippedVersions, + "the migration plan must list exactly the shipped versions, oldest first; a new " + + "version is appended to `shippedVersions` in the same change, and nothing " + + "that shipped is ever removed, reordered or replaced") + } + + /// The schema the app opens stores with must be the last version of + /// the migration plan. Both are declared separately, and SwiftData + /// accepts a plan whose tail is newer than the schema it is asked to + /// migrate to, so a version appended to the plan but not made the + /// container's schema would leave the app writing the older shape while + /// every migration test targets it. + func testTheLiveSchemaIsTheMigrationPlansLastVersion() throws { + let last = try XCTUnwrap(DashMigrationPlan.schemas.last) + XCTAssertEqual( + Self.describe(DashModelContainer.schema.version), + Self.describe(last.versionIdentifier), + "DashModelContainer.schema must be built from the migration plan's last version") + } + /// The SQLite indexes of a store, one line per index: table, name and /// the statement that created it. Auto-indexes SQLite makes for its /// own constraints have no statement and are listed as such. @@ -279,11 +320,20 @@ final class DashModelMigrationTests: XCTestCase { XCTAssertEqual(sqlite3_prepare_v2(database, query, -1, &statement, nil), SQLITE_OK) defer { sqlite3_finalize(statement) } var rows = Set() - while sqlite3_step(statement) == SQLITE_ROW { + var step = sqlite3_step(statement) + while step == SQLITE_ROW { let table = String(cString: sqlite3_column_text(statement, 0)) let name = String(cString: sqlite3_column_text(statement, 1)) let sql = sqlite3_column_text(statement, 2).map { String(cString: $0) } ?? "(auto)" rows.insert("\(table) \(name): \(sql)") + step = sqlite3_step(statement) + } + // Anything but SQLITE_DONE (busy, corrupt, I/O, memory) means the + // listing is partial, and a partial listing must not be compared. + guard step == SQLITE_DONE else { + struct PartialListing: Error {} + XCTFail("\(url.lastPathComponent): index listing stopped with sqlite result \(step)") + throw PartialListing() } return rows } @@ -301,9 +351,11 @@ final class DashModelMigrationTests: XCTestCase { /// has every index a store built fresh at the live version has (the /// migration created what the live model declares). /// - /// The second is a superset check, not equality: a store rebuilt by a - /// migration can keep an index from an earlier layout, which costs a - /// little space and nothing else. + /// The second is a superset check, not equality: a migrated store can + /// keep an index from an earlier layout, or one the live model no + /// longer declares. That is not a compatibility problem, but it is not + /// free either (the index takes storage and is maintained on every + /// write), and this check does not look for it. @MainActor func testFixturesAndMigratedStoresCarryTheIndexesFreshStoresHave() throws { let directory = FileManager.default.temporaryDirectory From bcf46dd23adb54a9496376bae2d3340c24e355c9 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 9 Sep 2026 21:19:24 +0700 Subject: [PATCH 10/12] chore(swift-sdk): correct the frozen-model banner about what the checksum hashes The banner on every generated frozen model listed #Index among the checksum's inputs. Core Data leaves indexes out of entity version hashes; an index feeds the store's SQLite indexes instead, which the migration tests check separately. The generator's banner says so now and the 36 model files are regenerated from it; --check matches byte for byte. Co-Authored-By: Claude Fable 5.1 --- .../FrozenSchemas/DashSchemaV1+PersistentAccount.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentAssetLock.swift | 8 ++++---- .../DashSchemaV1+PersistentCoreAddress.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentDPNSName.swift | 8 ++++---- .../DashSchemaV1+PersistentDashpayContactProfile.swift | 8 ++++---- .../DashSchemaV1+PersistentDashpayContactRequest.swift | 8 ++++---- .../DashSchemaV1+PersistentDashpayIgnoredSender.swift | 8 ++++---- .../DashSchemaV1+PersistentDashpayPayment.swift | 8 ++++---- .../DashSchemaV1+PersistentDashpayProfile.swift | 8 ++++---- .../DashSchemaV1+PersistentDataContract.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentDocument.swift | 8 ++++---- .../DashSchemaV1+PersistentDocumentType.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentIdentity.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentIndex.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentInvitation.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentKeyword.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentMasternode.swift | 8 ++++---- .../DashSchemaV1+PersistentPendingInput.swift | 8 ++++---- .../DashSchemaV1+PersistentPlatformAddress.swift | 8 ++++---- ...ashSchemaV1+PersistentPlatformAddressesSyncState.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentProperty.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentPublicKey.swift | 8 ++++---- .../DashSchemaV1+PersistentShieldedActivity.swift | 8 ++++---- .../DashSchemaV1+PersistentShieldedNote.swift | 8 ++++---- .../DashSchemaV1+PersistentShieldedOutgoingNote.swift | 8 ++++---- .../DashSchemaV1+PersistentShieldedSyncState.swift | 8 ++++---- .../DashSchemaV1+PersistentShieldedViewingKey.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentToken.swift | 8 ++++---- .../DashSchemaV1+PersistentTokenBalance.swift | 8 ++++---- .../DashSchemaV1+PersistentTokenHistoryEvent.swift | 8 ++++---- .../DashSchemaV1+PersistentTransaction.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentTxo.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV1+PersistentWallet.swift | 8 ++++---- .../DashSchemaV1+PersistentWalletManagerMetadata.swift | 8 ++++---- .../DashSchemaV2+PersistentTrackedMasternode.swift | 8 ++++---- .../FrozenSchemas/DashSchemaV3+PersistentAssetLock.swift | 8 ++++---- packages/swift-sdk/scripts/freeze_schema_models.py | 8 ++++---- 37 files changed, 148 insertions(+), 148 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAccount.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAccount.swift index 3cababb0805..939b4527762 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAccount.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAccount.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentAccount` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentAccount { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAssetLock.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAssetLock.swift index 233959f2991..b042a5da7a8 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAssetLock.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentAssetLock.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentAssetLock` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 7127c38566. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentAssetLock { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentCoreAddress.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentCoreAddress.swift index c13d9ddc403..5ea85933116 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentCoreAddress.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentCoreAddress.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentCoreAddress` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentCoreAddress { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDPNSName.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDPNSName.swift index 0f67290ab1f..0a62bbb1d3e 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDPNSName.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDPNSName.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentDPNSName` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentDPNSName { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactProfile.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactProfile.swift index 155a68ce444..b1d42ed97c8 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactProfile.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactProfile.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentDashpayContactProfile` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentDashpayContactProfile { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactRequest.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactRequest.swift index 75aa5f3b4ee..1c4661ef170 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactRequest.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayContactRequest.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentDashpayContactRequest` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentDashpayContactRequest { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayIgnoredSender.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayIgnoredSender.swift index 242fbf1027d..445685f43de 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayIgnoredSender.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayIgnoredSender.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentDashpayIgnoredSender` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentDashpayIgnoredSender { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayPayment.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayPayment.swift index 319c209082f..89b18cbb51a 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayPayment.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayPayment.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentDashpayPayment` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentDashpayPayment { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayProfile.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayProfile.swift index 9eec0834195..341eda78560 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayProfile.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDashpayProfile.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentDashpayProfile` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentDashpayProfile { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDataContract.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDataContract.swift index 302be50b557..49e35d37776 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDataContract.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDataContract.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentDataContract` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentDataContract { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocument.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocument.swift index 6d9cc9b32f1..cebae0f140d 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocument.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocument.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentDocument` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentDocument { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocumentType.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocumentType.swift index 06db414ae4b..a58b4ff625d 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocumentType.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentDocumentType.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentDocumentType` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentDocumentType { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIdentity.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIdentity.swift index 7f8df4e4e11..363a6245d10 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIdentity.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIdentity.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentIdentity` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentIdentity { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIndex.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIndex.swift index a58f67a72c5..9688fb11540 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIndex.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentIndex.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentIndex` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentIndex { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentInvitation.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentInvitation.swift index 1738a04d3f8..fcdb604dec2 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentInvitation.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentInvitation.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentInvitation` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentInvitation { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentKeyword.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentKeyword.swift index d96d4244488..acfc92b9a38 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentKeyword.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentKeyword.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentKeyword` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentKeyword { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentMasternode.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentMasternode.swift index c1797e9b3fe..d39f405f069 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentMasternode.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentMasternode.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentMasternode` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentMasternode { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPendingInput.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPendingInput.swift index 45322c7894f..df3730338b2 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPendingInput.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPendingInput.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentPendingInput` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentPendingInput { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddress.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddress.swift index 6a457fdd2e8..a4e84c8d35d 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddress.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddress.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentPlatformAddress` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentPlatformAddress { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddressesSyncState.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddressesSyncState.swift index 350f0edc21c..6724d51380b 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddressesSyncState.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPlatformAddressesSyncState.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentPlatformAddressesSyncState` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentPlatformAddressesSyncState { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentProperty.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentProperty.swift index 9246487d673..69183b3c63a 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentProperty.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentProperty.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentProperty` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentProperty { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPublicKey.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPublicKey.swift index e9bbce743dd..a7a2bf3b023 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPublicKey.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentPublicKey.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentPublicKey` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentPublicKey { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedActivity.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedActivity.swift index 177340b2798..c595dee2a4f 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedActivity.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedActivity.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentShieldedActivity` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentShieldedActivity { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedNote.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedNote.swift index 19615909f5b..0beb2719a74 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedNote.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedNote.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentShieldedNote` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentShieldedNote { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedOutgoingNote.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedOutgoingNote.swift index f97b1bafbea..6954db1b097 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedOutgoingNote.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedOutgoingNote.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentShieldedOutgoingNote` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentShieldedOutgoingNote { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedSyncState.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedSyncState.swift index d944680abea..688a911d444 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedSyncState.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedSyncState.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentShieldedSyncState` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentShieldedSyncState { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedViewingKey.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedViewingKey.swift index ab2058653fd..624f2c8b27c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedViewingKey.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentShieldedViewingKey.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentShieldedViewingKey` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentShieldedViewingKey { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentToken.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentToken.swift index 2bef35d8fb8..890bcbca49a 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentToken.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentToken.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentToken` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentToken { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenBalance.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenBalance.swift index 097e968cd64..558a916d1cf 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenBalance.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenBalance.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentTokenBalance` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentTokenBalance { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenHistoryEvent.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenHistoryEvent.swift index f32e27eb254..9b58631a31e 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenHistoryEvent.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTokenHistoryEvent.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentTokenHistoryEvent` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentTokenHistoryEvent { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTransaction.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTransaction.swift index d3a25a6b9c3..834bfa42056 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTransaction.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTransaction.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentTransaction` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentTransaction { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTxo.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTxo.swift index 86095488bd6..2568f004c68 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTxo.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentTxo.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentTxo` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentTxo { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWallet.swift index 20292cdcedd..ccfabe3b740 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWallet.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentWallet` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentWallet { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWalletManagerMetadata.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWalletManagerMetadata.swift index 183c6c8c41c..7e328101e50 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWalletManagerMetadata.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV1+PersistentWalletManagerMetadata.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentWalletManagerMetadata` exactly as schema DashSchemaV1 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV1 { @Model final class PersistentWalletManagerMetadata { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV2+PersistentTrackedMasternode.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV2+PersistentTrackedMasternode.swift index a07502dc4d8..b4c09ccc3c1 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV2+PersistentTrackedMasternode.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV2+PersistentTrackedMasternode.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentTrackedMasternode` exactly as schema DashSchemaV2 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV2 { @Model final class PersistentTrackedMasternode { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV3+PersistentAssetLock.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV3+PersistentAssetLock.swift index 16db6310a7e..d54a4343f1f 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV3+PersistentAssetLock.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV3+PersistentAssetLock.swift @@ -4,10 +4,10 @@ import SwiftData // `PersistentAssetLock` exactly as schema DashSchemaV3 registered it, generated by // scripts/freeze_schema_models.py from the live model at commit 5f58417079. // Do not edit: every stored property, its optionality and default, and -// every @Attribute / @Relationship / #Index / #Unique here is an input to -// that version's checksum, and changing any of them re-breaks the stores -// this copy exists to keep openable. See the live model for what each -// column means. +// every @Attribute / @Relationship / #Unique here is an input to that +// version's checksum, and every #Index to the store's SQLite indexes; +// changing any of them re-breaks the stores this copy exists to keep +// openable. See the live model for what each column means. extension DashSchemaV3 { @Model final class PersistentAssetLock { diff --git a/packages/swift-sdk/scripts/freeze_schema_models.py b/packages/swift-sdk/scripts/freeze_schema_models.py index cf0042064aa..baf78f0f269 100755 --- a/packages/swift-sdk/scripts/freeze_schema_models.py +++ b/packages/swift-sdk/scripts/freeze_schema_models.py @@ -329,10 +329,10 @@ def render_model(freeze, sha, source, model, sibling): + f"// `{model}` exactly as schema {freeze.schema} registered it, generated by\n" f"// scripts/freeze_schema_models.py from the live model at commit {sha}.\n" "// Do not edit: every stored property, its optionality and default, and\n" - "// every @Attribute / @Relationship / #Index / #Unique here is an input to\n" - "// that version's checksum, and changing any of them re-breaks the stores\n" - "// this copy exists to keep openable. See the live model for what each\n" - "// column means.\n" + "// every @Attribute / @Relationship / #Unique here is an input to that\n" + "// version's checksum, and every #Index to the store's SQLite indexes;\n" + "// changing any of them re-breaks the stores this copy exists to keep\n" + "// openable. See the live model for what each column means.\n" f"extension {freeze.schema} {{\n" f"{indent(extract_class(source, model))}\n" "}\n" From fdb2ae5282a7b17ef14df79ed1923f4b489b7298 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 9 Sep 2026 23:21:30 +0700 Subject: [PATCH 11/12] test(swift-sdk): guard the live schema version with its own fixture store The fixture list covered the retired versions only, so a change to a live V4 model's shape without cutting V5 passed every guard: the V1 to V3 fixtures still hashed and opened, the plan and live-schema guards saw no change, and a store written by the previous V4 failed to open with Cocoa error 134504. That is the failure the freeze exists to prevent. Fixtures/SchemaStores/dash-v4.store is written by this build through DashModelContainer.create, with the rows every fixture carries, and the coverage assertion now requires a fixture for every entry of the shipped list, the live one included. The writer is committed as an env-gated test (DASH_SCHEMA_FIXTURE_OUTPUT), so changing the live shape before it ships, which is legitimate, means rewriting the live fixture on purpose in the same change; the class doc, the shipped-list doc and the container doc say so. The store is left in rollback-journal mode so it is one file that opens read-only from any directory, as the older fixtures are. Proven in the standalone harness: one optional stored property added to the live PersistentKeyword fails the hash test against dash-v4 (drifted entity PersistentKeyword, checksum differs) and the factory-open test reproduces the 134504, while both plan guards stay green as they should. The round-5 guards still fire on their probes. Version identifiers in the shipped list must now be unique; two enums declaring the same identifier are indistinguishable to the plan guard, and whether an enum still has the shape that shipped is decided by that version's fixture in the hash test, which the test doc states. Co-Authored-By: Claude Fable 5.1 --- .../Persistence/DashModelContainer.swift | 10 +- .../DashModelMigrationTests.swift | 162 +++++++++++++++--- .../Fixtures/SchemaStores/dash-v4.store | Bin 0 -> 663552 bytes 3 files changed, 145 insertions(+), 27 deletions(-) create mode 100644 packages/swift-sdk/SwiftTests/SwiftDashSDKTests/Fixtures/SchemaStores/dash-v4.store diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift index f393ed93372..2d66c4b8ea3 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift @@ -91,10 +91,12 @@ public enum DashModelContainer { /// the released versions must not. When the next property lands: freeze /// every model here into the version being retired /// (`scripts/freeze_schema_models.py`), add a version, add a stage, and - /// commit a store written by the build that shipped the retired version - /// under the test fixtures. `DashModelMigrationTests` proves a freeze - /// complete (in what the entity hash covers, plus its indexes) only - /// against such a store, and fails until every retired version has one. + /// commit a store written by this build for the new version under the + /// test fixtures (`DashModelMigrationTests.testWriteTheLiveSchemaFixtureStore`). + /// `DashModelMigrationTests` proves a version's shape (what the entity + /// hash covers, plus its indexes) only against such a store, for the + /// live version too: changing a model here before the version ships + /// means rewriting the live fixture on purpose in the same change. public static var modelTypes: [any PersistentModel.Type] { [ PersistentIdentity.self, diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift index 8f56afee398..38b3c966555 100644 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift @@ -10,18 +10,27 @@ import XCTest /// process from each registered version, and stores that OLDER BUILDS /// actually wrote. /// -/// The fixture stores under `Fixtures/SchemaStores/` were written by a build -/// of the persistence sources as of commit 5f58417079 — the last state -/// before V4, the state the frozen copies under `FrozenSchemas/` are -/// generated from — through that build's own `DashSchemaV1` / -/// `DashSchemaV2` / `DashSchemaV3`. They pin the frozen copies as that -/// pre-V4 build defined them, not what the original V1 release wrote (see -/// the `DashSchemaV1` doc for why those stores are expected to fail open -/// and be rebuilt). Each carries a wallet, -/// an account, a core address, two transactions, a TXO linked to both, a -/// pending input, an identity, a keyword, an asset lock and (from V2) a -/// tracked masternode — enough to exercise every relationship in the wallet -/// graph. +/// The fixture stores under `Fixtures/SchemaStores/` were written by the +/// builds that shipped each version: `dash-v1` to `dash-v3` by a build of +/// the persistence sources as of commit 5f58417079 — the last state before +/// V4, the state the frozen copies under `FrozenSchemas/` are generated +/// from — through that build's own `DashSchemaV1` / `DashSchemaV2` / +/// `DashSchemaV3`, and `dash-v4` by the build that registered V4, through +/// `DashModelContainer.create`. They pin the frozen copies as the pre-V4 +/// build defined them, not what the original V1 release wrote (see the +/// `DashSchemaV1` doc for why those stores are expected to fail open and +/// be rebuilt). Each carries a wallet, an account, a core address, two +/// transactions, a TXO linked to both, a pending input, an identity, a +/// keyword, an asset lock and (from V2) a tracked masternode — enough to +/// exercise every relationship in the wallet graph; the rows are the ones +/// `testWriteTheLiveSchemaFixtureStore` writes. +/// +/// The live version has a fixture too, so a change to a live model's +/// shape fails the hash test against that version's own store — the +/// failure a store in the field would otherwise report as Cocoa error +/// 134504. Changing the live shape before it ships is legitimate; doing +/// so means regenerating the live fixture on purpose, with +/// `testWriteTheLiveSchemaFixtureStore`, in the same change. /// /// A source store written in this process by `Schema(versionedSchema:)` /// cannot replace them: SwiftData binds an entity name to the first Swift @@ -57,6 +66,9 @@ final class DashModelMigrationTests: XCTestCase { Fixture( name: "dash-v3", version: DashSchemaV3.self, hasTrackedMasternode: true, assetLockRecipientIsExternal: true), + Fixture( + name: "dash-v4", version: DashSchemaV4.self, + hasTrackedMasternode: true, assetLockRecipientIsExternal: true), ] /// Every schema version that has ever shipped, oldest first, as @@ -64,8 +76,10 @@ final class DashModelMigrationTests: XCTestCase { /// that exist in the field, so it can never be removed from, reordered /// in, or replaced in the migration plan, and the plan is checked /// against this list rather than the other way round. Adding a version - /// to the plan is shipping it: append it here in the same change, and - /// give the version before it a fixture store in `fixtures`. + /// to the plan is shipping it: append it here in the same change and + /// give it a fixture store in `fixtures`, written by that build with + /// `testWriteTheLiveSchemaFixtureStore`. Every entry has a fixture, + /// the live one included. private static let shippedVersions = ["1.0.0", "2.0.0", "3.0.0", "4.0.0"] private static let fixtureWalletId = Data(repeating: 0x31, count: 32) @@ -231,18 +245,21 @@ final class DashModelMigrationTests: XCTestCase { /// Its reach is exactly the fixtures: it guards a version only once a /// store written by a build that shipped that version is committed /// under `Fixtures/SchemaStores/` and listed in `fixtures`. So the first - /// thing checked is that `fixtures` lists every retired version in - /// `shippedVersions`, once each: cutting a new schema version fails this - /// test until the fixture written by the build that shipped the retired - /// version is committed. The expectation comes from the append-only - /// list, not from the migration plan, so a plan that dropped a version - /// cannot shrink it (`testShippedSchemaVersionsStayInTheMigrationPlan`). + /// thing checked is that `fixtures` lists every version in + /// `shippedVersions`, the live one included, once each: cutting a new + /// schema version fails this test until its fixture is committed, and + /// changing a live model's shape fails it against the live fixture + /// until that fixture is deliberately rewritten. The expectation comes + /// from the append-only list, not from the migration plan, so a plan + /// that dropped a version cannot shrink it + /// (`testShippedSchemaVersionsStayInTheMigrationPlan`). func testFrozenVersionsBuiltAfterTheLiveSchemaHashLikeTheStoresTheyShipped() throws { XCTAssertEqual( Self.fixtures.map { Self.describe($0.version.versionIdentifier) }, - Array(Self.shippedVersions.dropLast()), - "every retired schema version needs a fixture store written by the build that " - + "shipped it, listed once in `fixtures`; without one its freeze is unguarded") + Self.shippedVersions, + "every shipped schema version, the live one included, needs a fixture store " + + "written by the build that shipped it, listed once in `fixtures`; without " + + "one its shape is unguarded") for fixture in Self.fixtures { let (directory, url) = try copyFixture(fixture) @@ -280,7 +297,16 @@ final class DashModelMigrationTests: XCTestCase { /// `shippedVersions`, never the plan itself, so editing the plan cannot /// move the goalposts; a version can only enter the plan by being /// appended to that list in the same change. + /// + /// Versions are compared by identifier, not by enum: two enums both + /// declaring `4.0.0` are indistinguishable here, so the identifiers in + /// the list must be unique, and whether the enum behind an identifier + /// still has the shape that shipped is decided by that version's + /// fixture in the hash test. func testShippedSchemaVersionsStayInTheMigrationPlan() { + XCTAssertEqual( + Set(Self.shippedVersions).count, Self.shippedVersions.count, + "a version identifier can ship once") XCTAssertEqual( DashMigrationPlan.schemas.map { Self.describe($0.versionIdentifier) }, Self.shippedVersions, @@ -303,6 +329,96 @@ final class DashModelMigrationTests: XCTestCase { "DashModelContainer.schema must be built from the migration plan's last version") } + /// Writes the live version's fixture store — the rows every fixture + /// carries, through `DashModelContainer.create`, so the file is what + /// this build ships. Skipped unless `DASH_SCHEMA_FIXTURE_OUTPUT` names a + /// directory to write into; run it on purpose when the live shape + /// changes before shipping, or when a new version is cut: + /// + /// DASH_SCHEMA_FIXTURE_OUTPUT=/some/dir swift test \ + /// --filter DashModelMigrationTests/testWriteTheLiveSchemaFixtureStore + /// + /// then move `dash-vN.store` into `Fixtures/SchemaStores/`. A retired + /// version's fixture is never rewritten: only the build that shipped it + /// could write it. + @MainActor + func testWriteTheLiveSchemaFixtureStore() throws { + guard let output = ProcessInfo.processInfo.environment["DASH_SCHEMA_FIXTURE_OUTPUT"] + else { + throw XCTSkip("set DASH_SCHEMA_FIXTURE_OUTPUT to write the live schema's fixture") + } + let version = try XCTUnwrap(DashMigrationPlan.schemas.last).versionIdentifier + let url = URL(fileURLWithPath: output, isDirectory: true) + .appendingPathComponent("dash-v\(version.major).store") + XCTAssertFalse( + FileManager.default.fileExists(atPath: url.path), "\(url.path) already exists") + + var container: ModelContainer? = try DashModelContainer.create(url: url) + let context = try XCTUnwrap(container?.mainContext) + let wallet = PersistentWallet( + walletId: Self.fixtureWalletId, network: .testnet, name: "fixture wallet", + syncedHeight: 120) + context.insert(wallet) + let account = PersistentAccount( + wallet: wallet, accountType: 0, accountIndex: 0, accountTypeName: "standard") + context.insert(account) + let address = PersistentCoreAddress( + address: "yFixtureAddress", poolTypeTag: 0, addressIndex: 0, derivationPath: "m/0") + address.account = account + context.insert(address) + let funding = PersistentTransaction( + txid: Self.fixtureFundingTxid, transactionData: Data([3, 0]), context: 2, + blockHeight: 100) + let spend = PersistentTransaction( + txid: Self.fixtureSpendTxid, transactionData: Data([3, 0]), context: 2, + blockHeight: 110) + context.insert(funding) + context.insert(spend) + account.involvedTransactions = [funding, spend] + let txo = PersistentTxo( + transaction: funding, vout: 0, amount: 1_000, address: "yFixtureAddress", + height: 100) + txo.walletId = Self.fixtureWalletId + txo.isSpent = true + txo.spendingTransaction = spend + txo.coreAddress = address + txo.account = account + context.insert(txo) + context.insert(PersistentPendingInput( + outpoint: Data(repeating: 0x11, count: 36), inputIndex: 0, + spendingTxid: Self.fixtureSpendTxid, spendingTransaction: spend, + walletId: Self.fixtureWalletId)) + let identity = PersistentIdentity( + identityId: Self.fixtureIdentityId, balance: 5, network: .testnet) + identity.wallet = wallet + context.insert(identity) + context.insert(PersistentKeyword(keyword: "preserved", contractId: "contract")) + let lock = PersistentAssetLock( + outPointHex: String(repeating: "ab", count: 32) + ":0", + walletId: Self.fixtureWalletId, transactionBytes: Data([1, 2, 3]), + fundingTypeRaw: 4, identityIndexRaw: -1, amountDuffs: 100_000, statusRaw: 4) + lock.recipientIsExternal = true + context.insert(lock) + context.insert(PersistentTrackedMasternode( + networkRaw: Network.testnet.rawValue, proTxHash: Data(repeating: 7, count: 32), + label: "fixture", addedAt: 1, snapshotJSON: "{}")) + try context.save() + container = nil + + // A fixture has to be one self-contained file that opens read-only + // from any directory, so the write-ahead log is folded back in and + // the store left in rollback-journal mode, which also removes the + // -wal and -shm sidecars. + var database: OpaquePointer? + XCTAssertEqual( + sqlite3_open_v2(url.path, &database, SQLITE_OPEN_READWRITE, nil), SQLITE_OK) + XCTAssertEqual(sqlite3_exec(database, "PRAGMA journal_mode=DELETE", nil, nil, nil), SQLITE_OK) + sqlite3_close(database) + XCTAssertFalse( + FileManager.default.fileExists(atPath: url.path + "-wal"), + "the store still has a write-ahead log") + } + /// The SQLite indexes of a store, one line per index: table, name and /// the statement that created it. Auto-indexes SQLite makes for its /// own constraints have no statement and are listed as such. diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/Fixtures/SchemaStores/dash-v4.store b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/Fixtures/SchemaStores/dash-v4.store new file mode 100644 index 0000000000000000000000000000000000000000..d0a9ea7aad866eb4a89bf2015e48958dbded29d8 GIT binary patch literal 663552 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 W Date: Thu, 10 Sep 2026 01:15:10 +0700 Subject: [PATCH 12/12] test(swift-sdk): refuse to rewrite an existing fixture store The writer asserted the output did not exist but carried on regardless, so a second run opened a container over the committed fixture, rewrote its checksum and left WAL sidecars beside it. The guard now returns before the store is opened. Docs only in effect; the fixture and the freeze detection are unchanged. --- .../SwiftDashSDKTests/DashModelMigrationTests.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift index 38b3c966555..7daaabbe52d 100644 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift @@ -350,8 +350,13 @@ final class DashModelMigrationTests: XCTestCase { let version = try XCTUnwrap(DashMigrationPlan.schemas.last).versionIdentifier let url = URL(fileURLWithPath: output, isDirectory: true) .appendingPathComponent("dash-v\(version.major).store") - XCTAssertFalse( - FileManager.default.fileExists(atPath: url.path), "\(url.path) already exists") + // Stop before opening: creating a container over an existing store rewrites its + // checksum and leaves WAL sidecars behind, so a second run would silently + // replace the committed fixture rather than refusing to. + guard !FileManager.default.fileExists(atPath: url.path) else { + XCTFail("\(url.path) already exists; delete it to rewrite the fixture") + return + } var container: ModelContainer? = try DashModelContainer.create(url: url) let context = try XCTUnwrap(container?.mainContext)