From c519d1b396860d9508407d001420635ce6ea7454 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 10 Sep 2026 14:37:27 -0500 Subject: [PATCH] feat(sdk): add contract-scoped authentication key support --- .../12.json | 4167 +++++++++++++++++ .../persistence/DashDatabaseMigrationTest.kt | 16 +- .../dashsdk/ffi/NativePersistenceBridge.kt | 9 +- .../dashsdk/ffi/TransactionsNative.kt | 5 +- .../dashsdk/identity/IdentityPubkeyCodec.kt | 14 +- .../dashsdk/identity/IdentityUpdates.kt | 12 +- .../dashsdk/persistence/DashDatabase.kt | 12 +- .../PlatformWalletPersistenceHandler.kt | 12 +- .../persistence/entities/PublicKeyEntity.kt | 3 + .../dashsdk/persistence/DashDatabaseTest.kt | 4 +- .../PlatformWalletPersistenceHandlerTest.kt | 66 +- .../src/identity_persistence.rs | 201 +- .../src/identity_registration_with_signer.rs | 44 +- .../src/identity_update.rs | 40 +- .../rs-platform-wallet-ffi/src/invitation.rs | 2 + .../src/managed_identity.rs | 49 + .../rs-platform-wallet-ffi/src/persistence.rs | 145 +- .../src/wallet_restore_types.rs | 10 +- packages/rs-sdk-ffi/src/identity/mod.rs | 2 +- packages/rs-sdk-ffi/src/identity/parse.rs | 83 + .../rs-unified-sdk-jni/src/persistence.rs | 23 +- .../rs-unified-sdk-jni/src/pubkey_rows.rs | 47 +- .../rs-unified-sdk-jni/src/transactions.rs | 6 +- .../SwiftDashSDK/DPP/DPPIdentity.swift | 50 +- .../Persistence/DashModelContainer.swift | 63 +- .../Persistence/DashSchemaFrozenModels.swift | 5 +- .../DashSchemaV4FrozenModels.swift | 1159 +++++ .../Models/PersistentPublicKey.swift | 16 +- .../PlatformWallet/ManagedIdentity.swift | 14 +- .../ManagedPlatformWallet.swift | 28 +- .../PlatformWalletPersistenceHandler.swift | 26 +- .../Services/IdentityKeyRefresher.swift | 53 +- .../Views/LoadIdentityView.swift | 8 +- .../Views/StorageRecordDetailViews.swift | 4 +- .../DashModelMigrationTests.swift | 89 +- .../src/data_contract/contract_bounds.rs | 129 +- packages/wasm-dpp2/src/lib.rs | 1 + .../tests/smoke/scoped-authentication.cjs | 56 + 38 files changed, 6422 insertions(+), 251 deletions(-) create mode 100644 packages/kotlin-sdk/sdk/schemas/org.dashfoundation.dashsdk.persistence.DashDatabase/12.json create mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaV4FrozenModels.swift create mode 100644 packages/wasm-sdk/tests/smoke/scoped-authentication.cjs diff --git a/packages/kotlin-sdk/sdk/schemas/org.dashfoundation.dashsdk.persistence.DashDatabase/12.json b/packages/kotlin-sdk/sdk/schemas/org.dashfoundation.dashsdk.persistence.DashDatabase/12.json new file mode 100644 index 00000000000..b5133e0f0e2 --- /dev/null +++ b/packages/kotlin-sdk/sdk/schemas/org.dashfoundation.dashsdk.persistence.DashDatabase/12.json @@ -0,0 +1,4167 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "26c744fad2c079be4f94d2e4fa0341bd", + "entities": [ + { + "tableName": "wallets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`walletId` BLOB NOT NULL, `walletGroupId` BLOB NOT NULL, `networkRaw` INTEGER, `name` TEXT, `walletDescription` TEXT, `birthHeight` INTEGER NOT NULL, `syncedHeight` INTEGER NOT NULL, `lastSynced` INTEGER NOT NULL, `lastAppliedChainLockBytes` BLOB, `lastAppliedChainLockHeight` INTEGER, `isImported` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`walletId`))", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "walletGroupId", + "columnName": "walletGroupId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "walletDescription", + "columnName": "walletDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "birthHeight", + "columnName": "birthHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncedHeight", + "columnName": "syncedHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSynced", + "columnName": "lastSynced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastAppliedChainLockBytes", + "columnName": "lastAppliedChainLockBytes", + "affinity": "BLOB" + }, + { + "fieldPath": "lastAppliedChainLockHeight", + "columnName": "lastAppliedChainLockHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "isImported", + "columnName": "isImported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "walletId" + ] + }, + "indices": [ + { + "name": "index_wallets_networkRaw", + "unique": false, + "columnNames": [ + "networkRaw" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_wallets_networkRaw` ON `${TABLE_NAME}` (`networkRaw`)" + }, + { + "name": "index_wallets_walletGroupId", + "unique": false, + "columnNames": [ + "walletGroupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_wallets_walletGroupId` ON `${TABLE_NAME}` (`walletGroupId`)" + } + ] + }, + { + "tableName": "accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `walletId` BLOB NOT NULL, `accountType` INTEGER NOT NULL, `accountIndex` INTEGER NOT NULL, `accountTypeName` TEXT NOT NULL, `balanceConfirmed` INTEGER NOT NULL, `balanceUnconfirmed` INTEGER NOT NULL, `externalHighestUsed` INTEGER NOT NULL, `internalHighestUsed` INTEGER NOT NULL, `standardTag` INTEGER NOT NULL, `registrationIndex` INTEGER NOT NULL, `keyClass` INTEGER NOT NULL, `userIdentityId` BLOB NOT NULL, `friendIdentityId` BLOB NOT NULL, `accountExtendedPubKeyBytes` BLOB, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, FOREIGN KEY(`walletId`) REFERENCES `wallets`(`walletId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "accountType", + "columnName": "accountType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountIndex", + "columnName": "accountIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountTypeName", + "columnName": "accountTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "balanceConfirmed", + "columnName": "balanceConfirmed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "balanceUnconfirmed", + "columnName": "balanceUnconfirmed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "externalHighestUsed", + "columnName": "externalHighestUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalHighestUsed", + "columnName": "internalHighestUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "standardTag", + "columnName": "standardTag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationIndex", + "columnName": "registrationIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyClass", + "columnName": "keyClass", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userIdentityId", + "columnName": "userIdentityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "friendIdentityId", + "columnName": "friendIdentityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "accountExtendedPubKeyBytes", + "columnName": "accountExtendedPubKeyBytes", + "affinity": "BLOB" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_accounts_walletId", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_accounts_walletId` ON `${TABLE_NAME}` (`walletId`)" + }, + { + "name": "index_accounts_walletId_accountType_accountIndex_standardTag_registrationIndex_keyClass_userIdentityId_friendIdentityId", + "unique": true, + "columnNames": [ + "walletId", + "accountType", + "accountIndex", + "standardTag", + "registrationIndex", + "keyClass", + "userIdentityId", + "friendIdentityId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_accounts_walletId_accountType_accountIndex_standardTag_registrationIndex_keyClass_userIdentityId_friendIdentityId` ON `${TABLE_NAME}` (`walletId`, `accountType`, `accountIndex`, `standardTag`, `registrationIndex`, `keyClass`, `userIdentityId`, `friendIdentityId`)" + }, + { + "name": "index_accounts_accountExtendedPubKeyBytes", + "unique": true, + "columnNames": [ + "accountExtendedPubKeyBytes" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_accounts_accountExtendedPubKeyBytes` ON `${TABLE_NAME}` (`accountExtendedPubKeyBytes`)" + } + ], + "foreignKeys": [ + { + "table": "wallets", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "walletId" + ], + "referencedColumns": [ + "walletId" + ] + } + ] + }, + { + "tableName": "transactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`txid` BLOB NOT NULL, `transactionData` BLOB NOT NULL, `context` INTEGER NOT NULL, `blockHeight` INTEGER NOT NULL, `blockHash` BLOB, `blockTimestamp` INTEGER NOT NULL, `blockPosition` INTEGER NOT NULL, `hasBlockPosition` INTEGER NOT NULL, `direction` INTEGER NOT NULL, `transactionType` TEXT NOT NULL, `transactionTypeKind` INTEGER NOT NULL, `netAmount` INTEGER NOT NULL, `fee` INTEGER, `label` TEXT NOT NULL, `firstSeen` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`txid`))", + "fields": [ + { + "fieldPath": "txid", + "columnName": "txid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "transactionData", + "columnName": "transactionData", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "context", + "columnName": "context", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockHeight", + "columnName": "blockHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockHash", + "columnName": "blockHash", + "affinity": "BLOB" + }, + { + "fieldPath": "blockTimestamp", + "columnName": "blockTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockPosition", + "columnName": "blockPosition", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasBlockPosition", + "columnName": "hasBlockPosition", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "direction", + "columnName": "direction", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "transactionType", + "columnName": "transactionType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "transactionTypeKind", + "columnName": "transactionTypeKind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "netAmount", + "columnName": "netAmount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fee", + "columnName": "fee", + "affinity": "INTEGER" + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstSeen", + "columnName": "firstSeen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "txid" + ] + }, + "indices": [ + { + "name": "index_transactions_firstSeen", + "unique": false, + "columnNames": [ + "firstSeen" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_transactions_firstSeen` ON `${TABLE_NAME}` (`firstSeen`)" + } + ] + }, + { + "tableName": "transaction_account_involvements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`transactionTxid` BLOB NOT NULL, `accountId` INTEGER NOT NULL, PRIMARY KEY(`transactionTxid`, `accountId`), FOREIGN KEY(`transactionTxid`) REFERENCES `transactions`(`txid`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`accountId`) REFERENCES `accounts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "transactionTxid", + "columnName": "transactionTxid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "transactionTxid", + "accountId" + ] + }, + "indices": [ + { + "name": "index_transaction_account_involvements_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_transaction_account_involvements_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "transactions", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "transactionTxid" + ], + "referencedColumns": [ + "txid" + ] + }, + { + "table": "accounts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "txos", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`outpoint` BLOB NOT NULL, `vout` INTEGER NOT NULL, `amount` INTEGER NOT NULL, `address` TEXT NOT NULL, `scriptPubKey` BLOB NOT NULL, `height` INTEGER NOT NULL, `isCoinbase` INTEGER NOT NULL, `isConfirmed` INTEGER NOT NULL, `isInstantLocked` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `isSpent` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `walletId` BLOB NOT NULL, `txid` BLOB, `spendingTxid` BLOB, `spendingInputIndex` INTEGER, `accountId` INTEGER, `coreAddressId` TEXT, `supersededByTxid` BLOB, PRIMARY KEY(`outpoint`), FOREIGN KEY(`txid`) REFERENCES `transactions`(`txid`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`spendingTxid`) REFERENCES `transactions`(`txid`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`accountId`) REFERENCES `accounts`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`coreAddressId`) REFERENCES `core_addresses`(`address`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "outpoint", + "columnName": "outpoint", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "vout", + "columnName": "vout", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scriptPubKey", + "columnName": "scriptPubKey", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCoinbase", + "columnName": "isCoinbase", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isConfirmed", + "columnName": "isConfirmed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isInstantLocked", + "columnName": "isInstantLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSpent", + "columnName": "isSpent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "txid", + "columnName": "txid", + "affinity": "BLOB" + }, + { + "fieldPath": "spendingTxid", + "columnName": "spendingTxid", + "affinity": "BLOB" + }, + { + "fieldPath": "spendingInputIndex", + "columnName": "spendingInputIndex", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + }, + { + "fieldPath": "coreAddressId", + "columnName": "coreAddressId", + "affinity": "TEXT" + }, + { + "fieldPath": "supersededByTxid", + "columnName": "supersededByTxid", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "outpoint" + ] + }, + "indices": [ + { + "name": "index_txos_walletId", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_txos_walletId` ON `${TABLE_NAME}` (`walletId`)" + }, + { + "name": "index_txos_txid", + "unique": false, + "columnNames": [ + "txid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_txos_txid` ON `${TABLE_NAME}` (`txid`)" + }, + { + "name": "index_txos_spendingTxid", + "unique": false, + "columnNames": [ + "spendingTxid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_txos_spendingTxid` ON `${TABLE_NAME}` (`spendingTxid`)" + }, + { + "name": "index_txos_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_txos_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_txos_coreAddressId", + "unique": false, + "columnNames": [ + "coreAddressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_txos_coreAddressId` ON `${TABLE_NAME}` (`coreAddressId`)" + } + ], + "foreignKeys": [ + { + "table": "transactions", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "txid" + ], + "referencedColumns": [ + "txid" + ] + }, + { + "table": "transactions", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "spendingTxid" + ], + "referencedColumns": [ + "txid" + ] + }, + { + "table": "accounts", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "core_addresses", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "coreAddressId" + ], + "referencedColumns": [ + "address" + ] + } + ] + }, + { + "tableName": "core_addresses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `publicKey` BLOB NOT NULL, `poolTypeTag` INTEGER NOT NULL, `addressIndex` INTEGER NOT NULL, `derivationPath` TEXT NOT NULL, `isUsed` INTEGER NOT NULL, `firstSeenHeight` INTEGER NOT NULL, `lastSeenHeight` INTEGER NOT NULL, `balance` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `accountId` INTEGER, PRIMARY KEY(`address`), FOREIGN KEY(`accountId`) REFERENCES `accounts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "poolTypeTag", + "columnName": "poolTypeTag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addressIndex", + "columnName": "addressIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "derivationPath", + "columnName": "derivationPath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isUsed", + "columnName": "isUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstSeenHeight", + "columnName": "firstSeenHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSeenHeight", + "columnName": "lastSeenHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "balance", + "columnName": "balance", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "address" + ] + }, + "indices": [ + { + "name": "index_core_addresses_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_core_addresses_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "accounts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "asset_locks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`outPointHex` TEXT NOT NULL, `walletId` BLOB NOT NULL, `transactionBytes` BLOB NOT NULL, `fundingTypeRaw` INTEGER NOT NULL, `identityIndexRaw` INTEGER NOT NULL, `accountIndexRaw` INTEGER NOT NULL, `amountDuffs` INTEGER NOT NULL, `statusRaw` INTEGER NOT NULL, `proofBytes` BLOB, `recipientPlatformAddressHash` BLOB, `recipientPlatformAddressType` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`outPointHex`))", + "fields": [ + { + "fieldPath": "outPointHex", + "columnName": "outPointHex", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "transactionBytes", + "columnName": "transactionBytes", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "fundingTypeRaw", + "columnName": "fundingTypeRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identityIndexRaw", + "columnName": "identityIndexRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountIndexRaw", + "columnName": "accountIndexRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amountDuffs", + "columnName": "amountDuffs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "statusRaw", + "columnName": "statusRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proofBytes", + "columnName": "proofBytes", + "affinity": "BLOB" + }, + { + "fieldPath": "recipientPlatformAddressHash", + "columnName": "recipientPlatformAddressHash", + "affinity": "BLOB" + }, + { + "fieldPath": "recipientPlatformAddressType", + "columnName": "recipientPlatformAddressType", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "outPointHex" + ] + }, + "indices": [ + { + "name": "index_asset_locks_walletId", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_asset_locks_walletId` ON `${TABLE_NAME}` (`walletId`)" + } + ] + }, + { + "tableName": "invitations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`outPointHex` TEXT NOT NULL, `rawOutPoint` BLOB NOT NULL, `walletId` BLOB NOT NULL, `fundingIndexRaw` INTEGER NOT NULL, `amountDuffs` INTEGER NOT NULL, `expiryUnix` INTEGER NOT NULL, `createdAtSecs` INTEGER NOT NULL, `hasInviter` INTEGER NOT NULL, `statusRaw` INTEGER NOT NULL, `reclaimInFlight` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`outPointHex`))", + "fields": [ + { + "fieldPath": "outPointHex", + "columnName": "outPointHex", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rawOutPoint", + "columnName": "rawOutPoint", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "fundingIndexRaw", + "columnName": "fundingIndexRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amountDuffs", + "columnName": "amountDuffs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expiryUnix", + "columnName": "expiryUnix", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtSecs", + "columnName": "createdAtSecs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasInviter", + "columnName": "hasInviter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "statusRaw", + "columnName": "statusRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reclaimInFlight", + "columnName": "reclaimInFlight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "outPointHex" + ] + }, + "indices": [ + { + "name": "index_invitations_walletId", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_invitations_walletId` ON `${TABLE_NAME}` (`walletId`)" + } + ] + }, + { + "tableName": "identities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`identityId` BLOB NOT NULL, `balance` INTEGER NOT NULL, `revision` INTEGER NOT NULL, `isLocal` INTEGER NOT NULL, `alias` TEXT, `dpnsName` TEXT, `mainDpnsName` TEXT, `identityType` TEXT NOT NULL, `votingPrivateKeyIdentifier` TEXT, `ownerPrivateKeyIdentifier` TEXT, `payoutPrivateKeyIdentifier` TEXT, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `lastSyncedAt` INTEGER, `networkRaw` INTEGER NOT NULL, `walletId` BLOB, `identityIndex` INTEGER NOT NULL, PRIMARY KEY(`identityId`), FOREIGN KEY(`walletId`) REFERENCES `wallets`(`walletId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "identityId", + "columnName": "identityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "balance", + "columnName": "balance", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "revision", + "columnName": "revision", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocal", + "columnName": "isLocal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alias", + "columnName": "alias", + "affinity": "TEXT" + }, + { + "fieldPath": "dpnsName", + "columnName": "dpnsName", + "affinity": "TEXT" + }, + { + "fieldPath": "mainDpnsName", + "columnName": "mainDpnsName", + "affinity": "TEXT" + }, + { + "fieldPath": "identityType", + "columnName": "identityType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "votingPrivateKeyIdentifier", + "columnName": "votingPrivateKeyIdentifier", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerPrivateKeyIdentifier", + "columnName": "ownerPrivateKeyIdentifier", + "affinity": "TEXT" + }, + { + "fieldPath": "payoutPrivateKeyIdentifier", + "columnName": "payoutPrivateKeyIdentifier", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSyncedAt", + "columnName": "lastSyncedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB" + }, + { + "fieldPath": "identityIndex", + "columnName": "identityIndex", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "identityId" + ] + }, + "indices": [ + { + "name": "index_identities_networkRaw", + "unique": false, + "columnNames": [ + "networkRaw" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_identities_networkRaw` ON `${TABLE_NAME}` (`networkRaw`)" + }, + { + "name": "index_identities_walletId", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_identities_walletId` ON `${TABLE_NAME}` (`walletId`)" + } + ], + "foreignKeys": [ + { + "table": "wallets", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "walletId" + ], + "referencedColumns": [ + "walletId" + ] + } + ] + }, + { + "tableName": "public_keys", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `keyId` INTEGER NOT NULL, `purpose` TEXT NOT NULL, `securityLevel` TEXT NOT NULL, `keyType` TEXT NOT NULL, `readOnly` INTEGER NOT NULL, `disabledAt` INTEGER, `publicKeyData` BLOB NOT NULL, `contractBoundsData` BLOB, `contractBoundsDocumentTypeName` TEXT, `contractBoundsScope` BLOB, `privateKeyKeychainIdentifier` TEXT, `derivationIdentityIndex` INTEGER, `derivationKeyIndex` INTEGER, `identityId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `lastAccessed` INTEGER, `identityIdData` BLOB, FOREIGN KEY(`identityIdData`) REFERENCES `identities`(`identityId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "purpose", + "columnName": "purpose", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "securityLevel", + "columnName": "securityLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyType", + "columnName": "keyType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readOnly", + "columnName": "readOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "disabledAt", + "columnName": "disabledAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "publicKeyData", + "columnName": "publicKeyData", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "contractBoundsData", + "columnName": "contractBoundsData", + "affinity": "BLOB" + }, + { + "fieldPath": "contractBoundsDocumentTypeName", + "columnName": "contractBoundsDocumentTypeName", + "affinity": "TEXT" + }, + { + "fieldPath": "contractBoundsScope", + "columnName": "contractBoundsScope", + "affinity": "BLOB" + }, + { + "fieldPath": "privateKeyKeychainIdentifier", + "columnName": "privateKeyKeychainIdentifier", + "affinity": "TEXT" + }, + { + "fieldPath": "derivationIdentityIndex", + "columnName": "derivationIdentityIndex", + "affinity": "INTEGER" + }, + { + "fieldPath": "derivationKeyIndex", + "columnName": "derivationKeyIndex", + "affinity": "INTEGER" + }, + { + "fieldPath": "identityId", + "columnName": "identityId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastAccessed", + "columnName": "lastAccessed", + "affinity": "INTEGER" + }, + { + "fieldPath": "identityIdData", + "columnName": "identityIdData", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_public_keys_identityId_keyId", + "unique": false, + "columnNames": [ + "identityId", + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_public_keys_identityId_keyId` ON `${TABLE_NAME}` (`identityId`, `keyId`)" + }, + { + "name": "index_public_keys_identityIdData", + "unique": false, + "columnNames": [ + "identityIdData" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_public_keys_identityIdData` ON `${TABLE_NAME}` (`identityIdData`)" + }, + { + "name": "index_public_keys_publicKeyData", + "unique": false, + "columnNames": [ + "publicKeyData" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_public_keys_publicKeyData` ON `${TABLE_NAME}` (`publicKeyData`)" + } + ], + "foreignKeys": [ + { + "table": "identities", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "identityIdData" + ], + "referencedColumns": [ + "identityId" + ] + } + ] + }, + { + "tableName": "dpns_names", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkRaw` INTEGER NOT NULL, `label` TEXT NOT NULL, `normalizedLabel` TEXT NOT NULL, `parentDomainName` TEXT NOT NULL, `normalizedParentDomainName` TEXT NOT NULL, `acquiredAt` INTEGER NOT NULL, `identityId` BLOB NOT NULL, `documentId` BLOB, `isOwned` INTEGER NOT NULL, `priceCredits` INTEGER, `saleStatusRaw` INTEGER NOT NULL, `counterpartyIdentityId` BLOB, `documentCreatedAtMs` INTEGER NOT NULL, `documentUpdatedAtMs` INTEGER NOT NULL, `documentTransferredAtMs` INTEGER NOT NULL, `marketplaceUpdatedAt` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`networkRaw`, `normalizedParentDomainName`, `normalizedLabel`), FOREIGN KEY(`identityId`) REFERENCES `identities`(`identityId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "normalizedLabel", + "columnName": "normalizedLabel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentDomainName", + "columnName": "parentDomainName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "normalizedParentDomainName", + "columnName": "normalizedParentDomainName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "acquiredAt", + "columnName": "acquiredAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identityId", + "columnName": "identityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "documentId", + "columnName": "documentId", + "affinity": "BLOB" + }, + { + "fieldPath": "isOwned", + "columnName": "isOwned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "priceCredits", + "columnName": "priceCredits", + "affinity": "INTEGER" + }, + { + "fieldPath": "saleStatusRaw", + "columnName": "saleStatusRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "counterpartyIdentityId", + "columnName": "counterpartyIdentityId", + "affinity": "BLOB" + }, + { + "fieldPath": "documentCreatedAtMs", + "columnName": "documentCreatedAtMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentUpdatedAtMs", + "columnName": "documentUpdatedAtMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentTransferredAtMs", + "columnName": "documentTransferredAtMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "marketplaceUpdatedAt", + "columnName": "marketplaceUpdatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "networkRaw", + "normalizedParentDomainName", + "normalizedLabel" + ] + }, + "indices": [ + { + "name": "index_dpns_names_identityId", + "unique": false, + "columnNames": [ + "identityId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dpns_names_identityId` ON `${TABLE_NAME}` (`identityId`)" + }, + { + "name": "index_dpns_names_documentId", + "unique": false, + "columnNames": [ + "documentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dpns_names_documentId` ON `${TABLE_NAME}` (`documentId`)" + } + ], + "foreignKeys": [ + { + "table": "identities", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "identityId" + ], + "referencedColumns": [ + "identityId" + ] + } + ] + }, + { + "tableName": "dashpay_profiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkRaw` INTEGER NOT NULL, `identityId` BLOB NOT NULL, `displayName` TEXT, `publicMessage` TEXT, `bio` TEXT, `avatarUrl` TEXT, `avatarHash` BLOB, `avatarFingerprint` BLOB, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`networkRaw`, `identityId`), FOREIGN KEY(`identityId`) REFERENCES `identities`(`identityId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identityId", + "columnName": "identityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "publicMessage", + "columnName": "publicMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarHash", + "columnName": "avatarHash", + "affinity": "BLOB" + }, + { + "fieldPath": "avatarFingerprint", + "columnName": "avatarFingerprint", + "affinity": "BLOB" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "networkRaw", + "identityId" + ] + }, + "indices": [ + { + "name": "index_dashpay_profiles_identityId", + "unique": false, + "columnNames": [ + "identityId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dashpay_profiles_identityId` ON `${TABLE_NAME}` (`identityId`)" + } + ], + "foreignKeys": [ + { + "table": "identities", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "identityId" + ], + "referencedColumns": [ + "identityId" + ] + } + ] + }, + { + "tableName": "dashpay_contact_requests", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkRaw` INTEGER NOT NULL, `ownerIdentityId` BLOB NOT NULL, `contactIdentityId` BLOB NOT NULL, `isOutgoing` INTEGER NOT NULL, `senderKeyIndex` INTEGER NOT NULL, `recipientKeyIndex` INTEGER NOT NULL, `accountReference` INTEGER NOT NULL, `encryptedPublicKey` BLOB NOT NULL, `encryptedAccountLabel` BLOB, `autoAcceptProof` BLOB, `coreHeightCreatedAt` INTEGER NOT NULL, `createdAtMillis` INTEGER NOT NULL, `paymentChannelBroken` INTEGER NOT NULL DEFAULT 0, `contactAlias` TEXT, `contactNote` TEXT, `contactHidden` INTEGER NOT NULL DEFAULT 0, `contactAccountLabel` TEXT, `contactAcceptedAccounts` BLOB, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`networkRaw`, `ownerIdentityId`, `contactIdentityId`, `isOutgoing`), FOREIGN KEY(`ownerIdentityId`) REFERENCES `identities`(`identityId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerIdentityId", + "columnName": "ownerIdentityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "contactIdentityId", + "columnName": "contactIdentityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "isOutgoing", + "columnName": "isOutgoing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderKeyIndex", + "columnName": "senderKeyIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipientKeyIndex", + "columnName": "recipientKeyIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountReference", + "columnName": "accountReference", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptedPublicKey", + "columnName": "encryptedPublicKey", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "encryptedAccountLabel", + "columnName": "encryptedAccountLabel", + "affinity": "BLOB" + }, + { + "fieldPath": "autoAcceptProof", + "columnName": "autoAcceptProof", + "affinity": "BLOB" + }, + { + "fieldPath": "coreHeightCreatedAt", + "columnName": "coreHeightCreatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMillis", + "columnName": "createdAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "paymentChannelBroken", + "columnName": "paymentChannelBroken", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "contactAlias", + "columnName": "contactAlias", + "affinity": "TEXT" + }, + { + "fieldPath": "contactNote", + "columnName": "contactNote", + "affinity": "TEXT" + }, + { + "fieldPath": "contactHidden", + "columnName": "contactHidden", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "contactAccountLabel", + "columnName": "contactAccountLabel", + "affinity": "TEXT" + }, + { + "fieldPath": "contactAcceptedAccounts", + "columnName": "contactAcceptedAccounts", + "affinity": "BLOB" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "networkRaw", + "ownerIdentityId", + "contactIdentityId", + "isOutgoing" + ] + }, + "indices": [ + { + "name": "index_dashpay_contact_requests_ownerIdentityId", + "unique": false, + "columnNames": [ + "ownerIdentityId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dashpay_contact_requests_ownerIdentityId` ON `${TABLE_NAME}` (`ownerIdentityId`)" + } + ], + "foreignKeys": [ + { + "table": "identities", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "ownerIdentityId" + ], + "referencedColumns": [ + "identityId" + ] + } + ] + }, + { + "tableName": "dashpay_ignored_senders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkRaw` INTEGER NOT NULL, `ownerIdentityId` BLOB NOT NULL, `ignoredSenderId` BLOB NOT NULL, `ignoredAt` INTEGER NOT NULL, PRIMARY KEY(`networkRaw`, `ownerIdentityId`, `ignoredSenderId`), FOREIGN KEY(`ownerIdentityId`) REFERENCES `identities`(`identityId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerIdentityId", + "columnName": "ownerIdentityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "ignoredSenderId", + "columnName": "ignoredSenderId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "ignoredAt", + "columnName": "ignoredAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "networkRaw", + "ownerIdentityId", + "ignoredSenderId" + ] + }, + "indices": [ + { + "name": "index_dashpay_ignored_senders_ownerIdentityId", + "unique": false, + "columnNames": [ + "ownerIdentityId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dashpay_ignored_senders_ownerIdentityId` ON `${TABLE_NAME}` (`ownerIdentityId`)" + } + ], + "foreignKeys": [ + { + "table": "identities", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "ownerIdentityId" + ], + "referencedColumns": [ + "identityId" + ] + } + ] + }, + { + "tableName": "dashpay_contact_profiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkRaw` INTEGER NOT NULL, `ownerIdentityId` BLOB NOT NULL, `contactIdentityId` BLOB NOT NULL, `displayName` TEXT, `publicMessage` TEXT, `bio` TEXT, `avatarUrl` TEXT, `avatarHash` BLOB, `avatarFingerprint` BLOB, `checkedAtMs` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`networkRaw`, `ownerIdentityId`, `contactIdentityId`), FOREIGN KEY(`ownerIdentityId`) REFERENCES `identities`(`identityId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerIdentityId", + "columnName": "ownerIdentityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "contactIdentityId", + "columnName": "contactIdentityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "publicMessage", + "columnName": "publicMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarHash", + "columnName": "avatarHash", + "affinity": "BLOB" + }, + { + "fieldPath": "avatarFingerprint", + "columnName": "avatarFingerprint", + "affinity": "BLOB" + }, + { + "fieldPath": "checkedAtMs", + "columnName": "checkedAtMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "networkRaw", + "ownerIdentityId", + "contactIdentityId" + ] + }, + "indices": [ + { + "name": "index_dashpay_contact_profiles_ownerIdentityId", + "unique": false, + "columnNames": [ + "ownerIdentityId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dashpay_contact_profiles_ownerIdentityId` ON `${TABLE_NAME}` (`ownerIdentityId`)" + } + ], + "foreignKeys": [ + { + "table": "identities", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "ownerIdentityId" + ], + "referencedColumns": [ + "identityId" + ] + } + ] + }, + { + "tableName": "dashpay_payments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkRaw` INTEGER NOT NULL, `ownerIdentityId` BLOB NOT NULL, `counterpartyIdentityId` BLOB NOT NULL, `amountDuffs` INTEGER NOT NULL, `directionRaw` INTEGER NOT NULL, `statusRaw` INTEGER NOT NULL, `txid` TEXT NOT NULL, `memo` TEXT, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`networkRaw`, `ownerIdentityId`, `txid`), FOREIGN KEY(`ownerIdentityId`) REFERENCES `identities`(`identityId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerIdentityId", + "columnName": "ownerIdentityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "counterpartyIdentityId", + "columnName": "counterpartyIdentityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "amountDuffs", + "columnName": "amountDuffs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "directionRaw", + "columnName": "directionRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "statusRaw", + "columnName": "statusRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "txid", + "columnName": "txid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "memo", + "columnName": "memo", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "networkRaw", + "ownerIdentityId", + "txid" + ] + }, + "indices": [ + { + "name": "index_dashpay_payments_ownerIdentityId", + "unique": false, + "columnNames": [ + "ownerIdentityId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dashpay_payments_ownerIdentityId` ON `${TABLE_NAME}` (`ownerIdentityId`)" + } + ], + "foreignKeys": [ + { + "table": "identities", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "ownerIdentityId" + ], + "referencedColumns": [ + "identityId" + ] + } + ] + }, + { + "tableName": "data_contracts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `name` TEXT NOT NULL, `serializedContract` BLOB NOT NULL, `createdAt` INTEGER NOT NULL, `lastAccessedAt` INTEGER NOT NULL, `binarySerialization` BLOB, `version` INTEGER, `ownerId` BLOB, `contractDescription` TEXT, `schemaData` BLOB NOT NULL, `documentTypesData` BLOB NOT NULL, `groupsData` BLOB, `networkRaw` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `lastSyncedAt` INTEGER, `canBeDeleted` INTEGER NOT NULL, `readonly` INTEGER NOT NULL, `keepsHistory` INTEGER NOT NULL, `schemaDefs` INTEGER, `documentsKeepHistoryContractDefault` INTEGER NOT NULL, `documentsMutableContractDefault` INTEGER NOT NULL, `documentsCanBeDeletedContractDefault` INTEGER NOT NULL, `hasTokens` INTEGER NOT NULL, `tokensData` BLOB, `ownerIdentityId` BLOB, PRIMARY KEY(`id`), FOREIGN KEY(`ownerIdentityId`) REFERENCES `identities`(`identityId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedContract", + "columnName": "serializedContract", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastAccessedAt", + "columnName": "lastAccessedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "binarySerialization", + "columnName": "binarySerialization", + "affinity": "BLOB" + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "BLOB" + }, + { + "fieldPath": "contractDescription", + "columnName": "contractDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "schemaData", + "columnName": "schemaData", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "documentTypesData", + "columnName": "documentTypesData", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "groupsData", + "columnName": "groupsData", + "affinity": "BLOB" + }, + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSyncedAt", + "columnName": "lastSyncedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "canBeDeleted", + "columnName": "canBeDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readonly", + "columnName": "readonly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keepsHistory", + "columnName": "keepsHistory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "schemaDefs", + "columnName": "schemaDefs", + "affinity": "INTEGER" + }, + { + "fieldPath": "documentsKeepHistoryContractDefault", + "columnName": "documentsKeepHistoryContractDefault", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentsMutableContractDefault", + "columnName": "documentsMutableContractDefault", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentsCanBeDeletedContractDefault", + "columnName": "documentsCanBeDeletedContractDefault", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasTokens", + "columnName": "hasTokens", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tokensData", + "columnName": "tokensData", + "affinity": "BLOB" + }, + { + "fieldPath": "ownerIdentityId", + "columnName": "ownerIdentityId", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_data_contracts_networkRaw", + "unique": false, + "columnNames": [ + "networkRaw" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_data_contracts_networkRaw` ON `${TABLE_NAME}` (`networkRaw`)" + }, + { + "name": "index_data_contracts_ownerIdentityId", + "unique": false, + "columnNames": [ + "ownerIdentityId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_data_contracts_ownerIdentityId` ON `${TABLE_NAME}` (`ownerIdentityId`)" + } + ], + "foreignKeys": [ + { + "table": "identities", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "ownerIdentityId" + ], + "referencedColumns": [ + "identityId" + ] + } + ] + }, + { + "tableName": "document_types", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `contractId` BLOB NOT NULL, `name` TEXT NOT NULL, `schemaJSON` BLOB NOT NULL, `propertiesJSON` BLOB NOT NULL, `documentsKeepHistory` INTEGER NOT NULL, `documentsMutable` INTEGER NOT NULL, `documentsCanBeDeleted` INTEGER NOT NULL, `documentsTransferable` INTEGER NOT NULL, `requiredFieldsJSON` BLOB, `securityLevel` INTEGER NOT NULL, `tradeMode` INTEGER NOT NULL, `creationRestrictionMode` INTEGER NOT NULL, `requiresIdentityEncryptionBoundedKey` INTEGER NOT NULL, `requiresIdentityDecryptionBoundedKey` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `lastAccessedAt` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`contractId`) REFERENCES `data_contracts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "contractId", + "columnName": "contractId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schemaJSON", + "columnName": "schemaJSON", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "propertiesJSON", + "columnName": "propertiesJSON", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "documentsKeepHistory", + "columnName": "documentsKeepHistory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentsMutable", + "columnName": "documentsMutable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentsCanBeDeleted", + "columnName": "documentsCanBeDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentsTransferable", + "columnName": "documentsTransferable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requiredFieldsJSON", + "columnName": "requiredFieldsJSON", + "affinity": "BLOB" + }, + { + "fieldPath": "securityLevel", + "columnName": "securityLevel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tradeMode", + "columnName": "tradeMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationRestrictionMode", + "columnName": "creationRestrictionMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requiresIdentityEncryptionBoundedKey", + "columnName": "requiresIdentityEncryptionBoundedKey", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requiresIdentityDecryptionBoundedKey", + "columnName": "requiresIdentityDecryptionBoundedKey", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastAccessedAt", + "columnName": "lastAccessedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_document_types_contractId", + "unique": false, + "columnNames": [ + "contractId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_document_types_contractId` ON `${TABLE_NAME}` (`contractId`)" + } + ], + "foreignKeys": [ + { + "table": "data_contracts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contractId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "documents", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`documentId` TEXT NOT NULL, `documentType` TEXT NOT NULL, `revision` INTEGER NOT NULL, `data` BLOB NOT NULL, `contractId` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `contractIdData` BLOB NOT NULL, `ownerIdData` BLOB NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `transferredAt` INTEGER, `createdAtBlockHeight` INTEGER, `updatedAtBlockHeight` INTEGER, `transferredAtBlockHeight` INTEGER, `createdAtCoreBlockHeight` INTEGER, `updatedAtCoreBlockHeight` INTEGER, `transferredAtCoreBlockHeight` INTEGER, `networkRaw` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `localCreatedAt` INTEGER NOT NULL, `localUpdatedAt` INTEGER NOT NULL, `documentTypeRelationId` BLOB, `dataContractId` BLOB, `ownerIdentityId` BLOB, PRIMARY KEY(`documentId`), FOREIGN KEY(`documentTypeRelationId`) REFERENCES `document_types`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`dataContractId`) REFERENCES `data_contracts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`ownerIdentityId`) REFERENCES `identities`(`identityId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "documentId", + "columnName": "documentId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "documentType", + "columnName": "documentType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revision", + "columnName": "revision", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "contractId", + "columnName": "contractId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contractIdData", + "columnName": "contractIdData", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "ownerIdData", + "columnName": "ownerIdData", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "transferredAt", + "columnName": "transferredAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdAtBlockHeight", + "columnName": "createdAtBlockHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "updatedAtBlockHeight", + "columnName": "updatedAtBlockHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "transferredAtBlockHeight", + "columnName": "transferredAtBlockHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdAtCoreBlockHeight", + "columnName": "createdAtCoreBlockHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "updatedAtCoreBlockHeight", + "columnName": "updatedAtCoreBlockHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "transferredAtCoreBlockHeight", + "columnName": "transferredAtCoreBlockHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localCreatedAt", + "columnName": "localCreatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUpdatedAt", + "columnName": "localUpdatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentTypeRelationId", + "columnName": "documentTypeRelationId", + "affinity": "BLOB" + }, + { + "fieldPath": "dataContractId", + "columnName": "dataContractId", + "affinity": "BLOB" + }, + { + "fieldPath": "ownerIdentityId", + "columnName": "ownerIdentityId", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "documentId" + ] + }, + "indices": [ + { + "name": "index_documents_networkRaw", + "unique": false, + "columnNames": [ + "networkRaw" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_documents_networkRaw` ON `${TABLE_NAME}` (`networkRaw`)" + }, + { + "name": "index_documents_contractId", + "unique": false, + "columnNames": [ + "contractId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_documents_contractId` ON `${TABLE_NAME}` (`contractId`)" + }, + { + "name": "index_documents_ownerId", + "unique": false, + "columnNames": [ + "ownerId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_documents_ownerId` ON `${TABLE_NAME}` (`ownerId`)" + }, + { + "name": "index_documents_documentTypeRelationId", + "unique": false, + "columnNames": [ + "documentTypeRelationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_documents_documentTypeRelationId` ON `${TABLE_NAME}` (`documentTypeRelationId`)" + }, + { + "name": "index_documents_dataContractId", + "unique": false, + "columnNames": [ + "dataContractId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_documents_dataContractId` ON `${TABLE_NAME}` (`dataContractId`)" + }, + { + "name": "index_documents_ownerIdentityId", + "unique": false, + "columnNames": [ + "ownerIdentityId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_documents_ownerIdentityId` ON `${TABLE_NAME}` (`ownerIdentityId`)" + } + ], + "foreignKeys": [ + { + "table": "document_types", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "documentTypeRelationId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "data_contracts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "dataContractId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "identities", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "ownerIdentityId" + ], + "referencedColumns": [ + "identityId" + ] + } + ] + }, + { + "tableName": "indices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `contractId` BLOB NOT NULL, `documentTypeName` TEXT NOT NULL, `name` TEXT NOT NULL, `unique` INTEGER NOT NULL, `nullSearchable` INTEGER NOT NULL, `contested` INTEGER NOT NULL, `propertiesJSON` BLOB NOT NULL, `contestedDetailsJSON` BLOB, `createdAt` INTEGER NOT NULL, `documentTypeId` BLOB, PRIMARY KEY(`id`), FOREIGN KEY(`documentTypeId`) REFERENCES `document_types`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "contractId", + "columnName": "contractId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "documentTypeName", + "columnName": "documentTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unique", + "columnName": "unique", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nullSearchable", + "columnName": "nullSearchable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contested", + "columnName": "contested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "propertiesJSON", + "columnName": "propertiesJSON", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "contestedDetailsJSON", + "columnName": "contestedDetailsJSON", + "affinity": "BLOB" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentTypeId", + "columnName": "documentTypeId", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_indices_documentTypeId", + "unique": false, + "columnNames": [ + "documentTypeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_indices_documentTypeId` ON `${TABLE_NAME}` (`documentTypeId`)" + } + ], + "foreignKeys": [ + { + "table": "document_types", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "documentTypeId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `keyword` TEXT NOT NULL, `contractId` TEXT NOT NULL, `dataContractId` BLOB, PRIMARY KEY(`id`), FOREIGN KEY(`dataContractId`) REFERENCES `data_contracts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyword", + "columnName": "keyword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contractId", + "columnName": "contractId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dataContractId", + "columnName": "dataContractId", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_keywords_contractId", + "unique": false, + "columnNames": [ + "contractId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_keywords_contractId` ON `${TABLE_NAME}` (`contractId`)" + }, + { + "name": "index_keywords_dataContractId", + "unique": false, + "columnNames": [ + "dataContractId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_keywords_dataContractId` ON `${TABLE_NAME}` (`dataContractId`)" + } + ], + "foreignKeys": [ + { + "table": "data_contracts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "dataContractId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "properties", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `contractId` BLOB NOT NULL, `documentTypeName` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT, `contentMediaType` TEXT, `byteArray` INTEGER NOT NULL, `minItems` INTEGER, `maxItems` INTEGER, `pattern` TEXT, `minLength` INTEGER, `maxLength` INTEGER, `minValue` INTEGER, `maxValue` INTEGER, `fieldDescription` TEXT, `transient` INTEGER NOT NULL, `isRequired` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `documentTypeId` BLOB, PRIMARY KEY(`id`), FOREIGN KEY(`documentTypeId`) REFERENCES `document_types`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "contractId", + "columnName": "contractId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "documentTypeName", + "columnName": "documentTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT" + }, + { + "fieldPath": "contentMediaType", + "columnName": "contentMediaType", + "affinity": "TEXT" + }, + { + "fieldPath": "byteArray", + "columnName": "byteArray", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minItems", + "columnName": "minItems", + "affinity": "INTEGER" + }, + { + "fieldPath": "maxItems", + "columnName": "maxItems", + "affinity": "INTEGER" + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT" + }, + { + "fieldPath": "minLength", + "columnName": "minLength", + "affinity": "INTEGER" + }, + { + "fieldPath": "maxLength", + "columnName": "maxLength", + "affinity": "INTEGER" + }, + { + "fieldPath": "minValue", + "columnName": "minValue", + "affinity": "INTEGER" + }, + { + "fieldPath": "maxValue", + "columnName": "maxValue", + "affinity": "INTEGER" + }, + { + "fieldPath": "fieldDescription", + "columnName": "fieldDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "transient", + "columnName": "transient", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRequired", + "columnName": "isRequired", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentTypeId", + "columnName": "documentTypeId", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_properties_documentTypeId", + "unique": false, + "columnNames": [ + "documentTypeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_properties_documentTypeId` ON `${TABLE_NAME}` (`documentTypeId`)" + } + ], + "foreignKeys": [ + { + "table": "document_types", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "documentTypeId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "pending_inputs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `outpoint` BLOB NOT NULL, `inputIndex` INTEGER NOT NULL, `spendingTxid` BLOB NOT NULL, `spendingTransactionTxid` BLOB, `walletId` BLOB NOT NULL, `createdAt` INTEGER NOT NULL, `isSweptTombstone` INTEGER NOT NULL DEFAULT 0, `winnerMinedHeight` INTEGER, FOREIGN KEY(`spendingTransactionTxid`) REFERENCES `transactions`(`txid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "outpoint", + "columnName": "outpoint", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "inputIndex", + "columnName": "inputIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spendingTxid", + "columnName": "spendingTxid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "spendingTransactionTxid", + "columnName": "spendingTransactionTxid", + "affinity": "BLOB" + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSweptTombstone", + "columnName": "isSweptTombstone", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "winnerMinedHeight", + "columnName": "winnerMinedHeight", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_pending_inputs_outpoint", + "unique": false, + "columnNames": [ + "outpoint" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pending_inputs_outpoint` ON `${TABLE_NAME}` (`outpoint`)" + }, + { + "name": "index_pending_inputs_walletId", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pending_inputs_walletId` ON `${TABLE_NAME}` (`walletId`)" + }, + { + "name": "index_pending_inputs_spendingTransactionTxid", + "unique": false, + "columnNames": [ + "spendingTransactionTxid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pending_inputs_spendingTransactionTxid` ON `${TABLE_NAME}` (`spendingTransactionTxid`)" + }, + { + "name": "index_pending_inputs_spendingTxid", + "unique": false, + "columnNames": [ + "spendingTxid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pending_inputs_spendingTxid` ON `${TABLE_NAME}` (`spendingTxid`)" + }, + { + "name": "index_pending_inputs_walletId_isSweptTombstone_winnerMinedHeight", + "unique": false, + "columnNames": [ + "walletId", + "isSweptTombstone", + "winnerMinedHeight" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pending_inputs_walletId_isSweptTombstone_winnerMinedHeight` ON `${TABLE_NAME}` (`walletId`, `isSweptTombstone`, `winnerMinedHeight`)" + } + ], + "foreignKeys": [ + { + "table": "transactions", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "spendingTransactionTxid" + ], + "referencedColumns": [ + "txid" + ] + } + ] + }, + { + "tableName": "tokens", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `contractId` BLOB NOT NULL, `position` INTEGER NOT NULL, `name` TEXT NOT NULL, `baseSupply` TEXT NOT NULL, `maxSupply` TEXT, `decimals` INTEGER NOT NULL, `localizations` TEXT, `isPaused` INTEGER NOT NULL, `allowTransferToFrozenBalance` INTEGER NOT NULL, `keepsTransferHistory` INTEGER NOT NULL, `keepsFreezingHistory` INTEGER NOT NULL, `keepsMintingHistory` INTEGER NOT NULL, `keepsBurningHistory` INTEGER NOT NULL, `keepsDirectPricingHistory` INTEGER NOT NULL, `keepsDirectPurchaseHistory` INTEGER NOT NULL, `conventionsChangeRules` TEXT, `maxSupplyChangeRules` TEXT, `manualMintingRules` TEXT, `manualBurningRules` TEXT, `freezeRules` TEXT, `unfreezeRules` TEXT, `destroyFrozenFundsRules` TEXT, `emergencyActionRules` TEXT, `perpetualDistribution` TEXT, `preProgrammedDistribution` TEXT, `newTokensDestinationIdentity` BLOB, `mintingAllowChoosingDestination` INTEGER NOT NULL, `distributionChangeRules` TEXT, `tradeMode` TEXT NOT NULL, `tradeModeChangeRules` TEXT, `mainControlGroupPosition` INTEGER, `mainControlGroupCanBeModified` TEXT, `tokenDescription` TEXT, `createdAt` INTEGER NOT NULL, `lastUpdatedAt` INTEGER NOT NULL, `canManuallyMint` INTEGER NOT NULL, `canManuallyBurn` INTEGER NOT NULL, `canFreeze` INTEGER NOT NULL, `canUnfreeze` INTEGER NOT NULL, `canDestroyFrozenFunds` INTEGER NOT NULL, `hasEmergencyActions` INTEGER NOT NULL, `canChangeMaxSupply` INTEGER NOT NULL, `canChangeConventions` INTEGER NOT NULL, `canChangeTradeMode` INTEGER NOT NULL, `hasDistribution` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`contractId`) REFERENCES `data_contracts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "contractId", + "columnName": "contractId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseSupply", + "columnName": "baseSupply", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxSupply", + "columnName": "maxSupply", + "affinity": "TEXT" + }, + { + "fieldPath": "decimals", + "columnName": "decimals", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localizations", + "columnName": "localizations", + "affinity": "TEXT" + }, + { + "fieldPath": "isPaused", + "columnName": "isPaused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowTransferToFrozenBalance", + "columnName": "allowTransferToFrozenBalance", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keepsTransferHistory", + "columnName": "keepsTransferHistory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keepsFreezingHistory", + "columnName": "keepsFreezingHistory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keepsMintingHistory", + "columnName": "keepsMintingHistory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keepsBurningHistory", + "columnName": "keepsBurningHistory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keepsDirectPricingHistory", + "columnName": "keepsDirectPricingHistory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keepsDirectPurchaseHistory", + "columnName": "keepsDirectPurchaseHistory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conventionsChangeRules", + "columnName": "conventionsChangeRules", + "affinity": "TEXT" + }, + { + "fieldPath": "maxSupplyChangeRules", + "columnName": "maxSupplyChangeRules", + "affinity": "TEXT" + }, + { + "fieldPath": "manualMintingRules", + "columnName": "manualMintingRules", + "affinity": "TEXT" + }, + { + "fieldPath": "manualBurningRules", + "columnName": "manualBurningRules", + "affinity": "TEXT" + }, + { + "fieldPath": "freezeRules", + "columnName": "freezeRules", + "affinity": "TEXT" + }, + { + "fieldPath": "unfreezeRules", + "columnName": "unfreezeRules", + "affinity": "TEXT" + }, + { + "fieldPath": "destroyFrozenFundsRules", + "columnName": "destroyFrozenFundsRules", + "affinity": "TEXT" + }, + { + "fieldPath": "emergencyActionRules", + "columnName": "emergencyActionRules", + "affinity": "TEXT" + }, + { + "fieldPath": "perpetualDistribution", + "columnName": "perpetualDistribution", + "affinity": "TEXT" + }, + { + "fieldPath": "preProgrammedDistribution", + "columnName": "preProgrammedDistribution", + "affinity": "TEXT" + }, + { + "fieldPath": "newTokensDestinationIdentity", + "columnName": "newTokensDestinationIdentity", + "affinity": "BLOB" + }, + { + "fieldPath": "mintingAllowChoosingDestination", + "columnName": "mintingAllowChoosingDestination", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "distributionChangeRules", + "columnName": "distributionChangeRules", + "affinity": "TEXT" + }, + { + "fieldPath": "tradeMode", + "columnName": "tradeMode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tradeModeChangeRules", + "columnName": "tradeModeChangeRules", + "affinity": "TEXT" + }, + { + "fieldPath": "mainControlGroupPosition", + "columnName": "mainControlGroupPosition", + "affinity": "INTEGER" + }, + { + "fieldPath": "mainControlGroupCanBeModified", + "columnName": "mainControlGroupCanBeModified", + "affinity": "TEXT" + }, + { + "fieldPath": "tokenDescription", + "columnName": "tokenDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdatedAt", + "columnName": "lastUpdatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canManuallyMint", + "columnName": "canManuallyMint", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canManuallyBurn", + "columnName": "canManuallyBurn", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canFreeze", + "columnName": "canFreeze", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canUnfreeze", + "columnName": "canUnfreeze", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDestroyFrozenFunds", + "columnName": "canDestroyFrozenFunds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasEmergencyActions", + "columnName": "hasEmergencyActions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canChangeMaxSupply", + "columnName": "canChangeMaxSupply", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canChangeConventions", + "columnName": "canChangeConventions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canChangeTradeMode", + "columnName": "canChangeTradeMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasDistribution", + "columnName": "hasDistribution", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_tokens_contractId", + "unique": false, + "columnNames": [ + "contractId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tokens_contractId` ON `${TABLE_NAME}` (`contractId`)" + } + ], + "foreignKeys": [ + { + "table": "data_contracts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contractId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "token_balances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tokenId` TEXT NOT NULL, `identityId` BLOB NOT NULL, `balance` BLOB NOT NULL, `frozen` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `lastSyncedAt` INTEGER, `tokenName` TEXT, `tokenSymbol` TEXT, `tokenDecimals` INTEGER, `networkRaw` INTEGER NOT NULL, `identityRef` BLOB, `tokenRef` BLOB, FOREIGN KEY(`identityRef`) REFERENCES `identities`(`identityId`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`tokenRef`) REFERENCES `tokens`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tokenId", + "columnName": "tokenId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "identityId", + "columnName": "identityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "balance", + "columnName": "balance", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "frozen", + "columnName": "frozen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSyncedAt", + "columnName": "lastSyncedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "tokenName", + "columnName": "tokenName", + "affinity": "TEXT" + }, + { + "fieldPath": "tokenSymbol", + "columnName": "tokenSymbol", + "affinity": "TEXT" + }, + { + "fieldPath": "tokenDecimals", + "columnName": "tokenDecimals", + "affinity": "INTEGER" + }, + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identityRef", + "columnName": "identityRef", + "affinity": "BLOB" + }, + { + "fieldPath": "tokenRef", + "columnName": "tokenRef", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_token_balances_networkRaw", + "unique": false, + "columnNames": [ + "networkRaw" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_token_balances_networkRaw` ON `${TABLE_NAME}` (`networkRaw`)" + }, + { + "name": "index_token_balances_tokenId_identityId", + "unique": false, + "columnNames": [ + "tokenId", + "identityId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_token_balances_tokenId_identityId` ON `${TABLE_NAME}` (`tokenId`, `identityId`)" + }, + { + "name": "index_token_balances_identityId", + "unique": false, + "columnNames": [ + "identityId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_token_balances_identityId` ON `${TABLE_NAME}` (`identityId`)" + }, + { + "name": "index_token_balances_identityRef", + "unique": false, + "columnNames": [ + "identityRef" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_token_balances_identityRef` ON `${TABLE_NAME}` (`identityRef`)" + }, + { + "name": "index_token_balances_tokenRef", + "unique": false, + "columnNames": [ + "tokenRef" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_token_balances_tokenRef` ON `${TABLE_NAME}` (`tokenRef`)" + } + ], + "foreignKeys": [ + { + "table": "identities", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "identityRef" + ], + "referencedColumns": [ + "identityId" + ] + }, + { + "table": "tokens", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "tokenRef" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "token_history_events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `eventType` TEXT NOT NULL, `transactionId` BLOB, `blockHeight` INTEGER, `coreBlockHeight` INTEGER, `fromIdentity` BLOB, `toIdentity` BLOB, `performedByIdentity` BLOB NOT NULL, `amount` TEXT, `balanceBefore` TEXT, `balanceAfter` TEXT, `additionalDataJSON` BLOB, `eventDescription` TEXT, `createdAt` INTEGER NOT NULL, `eventTimestamp` INTEGER NOT NULL, `tokenRef` BLOB, PRIMARY KEY(`id`), FOREIGN KEY(`tokenRef`) REFERENCES `tokens`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventType", + "columnName": "eventType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "transactionId", + "columnName": "transactionId", + "affinity": "BLOB" + }, + { + "fieldPath": "blockHeight", + "columnName": "blockHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "coreBlockHeight", + "columnName": "coreBlockHeight", + "affinity": "INTEGER" + }, + { + "fieldPath": "fromIdentity", + "columnName": "fromIdentity", + "affinity": "BLOB" + }, + { + "fieldPath": "toIdentity", + "columnName": "toIdentity", + "affinity": "BLOB" + }, + { + "fieldPath": "performedByIdentity", + "columnName": "performedByIdentity", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "TEXT" + }, + { + "fieldPath": "balanceBefore", + "columnName": "balanceBefore", + "affinity": "TEXT" + }, + { + "fieldPath": "balanceAfter", + "columnName": "balanceAfter", + "affinity": "TEXT" + }, + { + "fieldPath": "additionalDataJSON", + "columnName": "additionalDataJSON", + "affinity": "BLOB" + }, + { + "fieldPath": "eventDescription", + "columnName": "eventDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eventTimestamp", + "columnName": "eventTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tokenRef", + "columnName": "tokenRef", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_token_history_events_tokenRef", + "unique": false, + "columnNames": [ + "tokenRef" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_token_history_events_tokenRef` ON `${TABLE_NAME}` (`tokenRef`)" + } + ], + "foreignKeys": [ + { + "table": "tokens", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "tokenRef" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "platform_addresses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `addressType` INTEGER NOT NULL, `addressHash` BLOB NOT NULL, `publicKey` BLOB NOT NULL, `accountIndex` INTEGER NOT NULL, `addressIndex` INTEGER NOT NULL, `derivationPath` TEXT NOT NULL, `isUsed` INTEGER NOT NULL, `balance` INTEGER NOT NULL, `nonce` INTEGER NOT NULL, `firstSeenHeight` INTEGER NOT NULL, `lastSeenHeight` INTEGER NOT NULL, `walletId` BLOB NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `accountId` INTEGER, PRIMARY KEY(`walletId`, `address`), FOREIGN KEY(`accountId`) REFERENCES `accounts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressType", + "columnName": "addressType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addressHash", + "columnName": "addressHash", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "accountIndex", + "columnName": "accountIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addressIndex", + "columnName": "addressIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "derivationPath", + "columnName": "derivationPath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isUsed", + "columnName": "isUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "balance", + "columnName": "balance", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nonce", + "columnName": "nonce", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstSeenHeight", + "columnName": "firstSeenHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSeenHeight", + "columnName": "lastSeenHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "walletId", + "address" + ] + }, + "indices": [ + { + "name": "index_platform_addresses_walletId_addressHash", + "unique": true, + "columnNames": [ + "walletId", + "addressHash" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_platform_addresses_walletId_addressHash` ON `${TABLE_NAME}` (`walletId`, `addressHash`)" + }, + { + "name": "index_platform_addresses_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_platform_addresses_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "accounts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "platform_addresses_sync_states", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`walletId` BLOB NOT NULL, `networkRaw` INTEGER NOT NULL, `syncHeight` INTEGER NOT NULL, `syncTimestamp` INTEGER NOT NULL, `lastKnownRecentBlock` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`walletId`))", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncHeight", + "columnName": "syncHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncTimestamp", + "columnName": "syncTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastKnownRecentBlock", + "columnName": "lastKnownRecentBlock", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "walletId" + ] + }, + "indices": [ + { + "name": "index_platform_addresses_sync_states_networkRaw", + "unique": false, + "columnNames": [ + "networkRaw" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_platform_addresses_sync_states_networkRaw` ON `${TABLE_NAME}` (`networkRaw`)" + } + ] + }, + { + "tableName": "shielded_notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`nullifier` BLOB NOT NULL, `walletId` BLOB NOT NULL, `accountIndex` INTEGER NOT NULL, `position` INTEGER NOT NULL, `cmx` BLOB NOT NULL, `blockHeight` INTEGER NOT NULL, `isSpent` INTEGER NOT NULL, `value` INTEGER NOT NULL, `noteData` BLOB NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`nullifier`))", + "fields": [ + { + "fieldPath": "nullifier", + "columnName": "nullifier", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "accountIndex", + "columnName": "accountIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cmx", + "columnName": "cmx", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "blockHeight", + "columnName": "blockHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSpent", + "columnName": "isSpent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "noteData", + "columnName": "noteData", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "nullifier" + ] + }, + "indices": [ + { + "name": "index_shielded_notes_walletId_accountIndex", + "unique": false, + "columnNames": [ + "walletId", + "accountIndex" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_shielded_notes_walletId_accountIndex` ON `${TABLE_NAME}` (`walletId`, `accountIndex`)" + } + ] + }, + { + "tableName": "shielded_outgoing_notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`walletId` BLOB NOT NULL, `accountIndex` INTEGER NOT NULL, `cmx` BLOB NOT NULL, `recipient` BLOB NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB NOT NULL, `blockHeight` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`walletId`, `accountIndex`, `cmx`))", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "accountIndex", + "columnName": "accountIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cmx", + "columnName": "cmx", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "memo", + "columnName": "memo", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "blockHeight", + "columnName": "blockHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "walletId", + "accountIndex", + "cmx" + ] + }, + "indices": [ + { + "name": "index_shielded_outgoing_notes_walletId_accountIndex", + "unique": false, + "columnNames": [ + "walletId", + "accountIndex" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_shielded_outgoing_notes_walletId_accountIndex` ON `${TABLE_NAME}` (`walletId`, `accountIndex`)" + } + ] + }, + { + "tableName": "shielded_activities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`walletId` BLOB NOT NULL, `accountIndex` INTEGER NOT NULL, `entryId` BLOB NOT NULL, `kindTag` INTEGER NOT NULL, `direction` INTEGER NOT NULL, `status` INTEGER NOT NULL, `amount` INTEGER NOT NULL, `fee` INTEGER NOT NULL, `hasFee` INTEGER NOT NULL, `blockHeight` INTEGER NOT NULL, `hasBlockHeight` INTEGER NOT NULL, `createdAtMs` INTEGER NOT NULL, `identityId` BLOB NOT NULL, `counterparty` BLOB NOT NULL, `memo` BLOB NOT NULL, `noteCmxs` BLOB NOT NULL, `spentNullifiers` BLOB NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`walletId`, `accountIndex`, `entryId`))", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "accountIndex", + "columnName": "accountIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryId", + "columnName": "entryId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "kindTag", + "columnName": "kindTag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "direction", + "columnName": "direction", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fee", + "columnName": "fee", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasFee", + "columnName": "hasFee", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockHeight", + "columnName": "blockHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasBlockHeight", + "columnName": "hasBlockHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMs", + "columnName": "createdAtMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identityId", + "columnName": "identityId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "counterparty", + "columnName": "counterparty", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "memo", + "columnName": "memo", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "noteCmxs", + "columnName": "noteCmxs", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "spentNullifiers", + "columnName": "spentNullifiers", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "walletId", + "accountIndex", + "entryId" + ] + }, + "indices": [ + { + "name": "index_shielded_activities_walletId_accountIndex", + "unique": false, + "columnNames": [ + "walletId", + "accountIndex" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_shielded_activities_walletId_accountIndex` ON `${TABLE_NAME}` (`walletId`, `accountIndex`)" + } + ] + }, + { + "tableName": "shielded_sync_states", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`walletId` BLOB NOT NULL, `accountIndex` INTEGER NOT NULL, `lastSyncedIndex` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`walletId`, `accountIndex`))", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "accountIndex", + "columnName": "accountIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSyncedIndex", + "columnName": "lastSyncedIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "walletId", + "accountIndex" + ] + }, + "indices": [ + { + "name": "index_shielded_sync_states_walletId", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_shielded_sync_states_walletId` ON `${TABLE_NAME}` (`walletId`)" + } + ] + }, + { + "tableName": "shielded_viewing_keys", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`walletId` BLOB NOT NULL, `accountIndex` INTEGER NOT NULL, `fvkBytes` BLOB NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`walletId`, `accountIndex`))", + "fields": [ + { + "fieldPath": "walletId", + "columnName": "walletId", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "accountIndex", + "columnName": "accountIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fvkBytes", + "columnName": "fvkBytes", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "walletId", + "accountIndex" + ] + }, + "indices": [ + { + "name": "index_shielded_viewing_keys_walletId", + "unique": false, + "columnNames": [ + "walletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_shielded_viewing_keys_walletId` ON `${TABLE_NAME}` (`walletId`)" + } + ] + }, + { + "tableName": "wallet_manager_metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkRaw` INTEGER NOT NULL, `combinedSyncHeight` INTEGER NOT NULL, `combinedSyncBlockHash` BLOB, `walletCount` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`networkRaw`))", + "fields": [ + { + "fieldPath": "networkRaw", + "columnName": "networkRaw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "combinedSyncHeight", + "columnName": "combinedSyncHeight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "combinedSyncBlockHash", + "columnName": "combinedSyncBlockHash", + "affinity": "BLOB" + }, + { + "fieldPath": "walletCount", + "columnName": "walletCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "networkRaw" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '26c744fad2c079be4f94d2e4fa0341bd')" + ] + } +} \ No newline at end of file diff --git a/packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseMigrationTest.kt b/packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseMigrationTest.kt index ef90b3804a1..25f871b6b2b 100644 --- a/packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseMigrationTest.kt +++ b/packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseMigrationTest.kt @@ -470,13 +470,21 @@ class DashDatabaseMigrationTest { db.close() } + @Test + fun migrate11To12AddsAuthenticationScope() { + helper.createDatabase(dbName, 11).close() + val db = helper.runMigrationsAndValidate(dbName, 12, true, DashDatabase.MIGRATION_11_12) + db.query("SELECT contractBoundsScope FROM public_keys").close() + db.close() + } + /** The requested contiguous path from the pre-u64 v4 schema to latest. */ @Test fun migrate4ToLatest() { helper.createDatabase(dbName, 4).close() helper.runMigrationsAndValidate( dbName, - 11, + 12, true, DashDatabase.MIGRATION_4_5, DashDatabase.MIGRATION_5_6, @@ -485,16 +493,17 @@ class DashDatabaseMigrationTest { DashDatabase.MIGRATION_8_9, DashDatabase.MIGRATION_9_10, DashDatabase.MIGRATION_10_11, + DashDatabase.MIGRATION_11_12, ).close() } - /** The full chain from v1 must also land on a valid v11 schema. */ + /** The full chain from v1 must also land on a valid v12 schema. */ @Test fun migrateAllTheWayFrom1() { helper.createDatabase(dbName, 1).close() helper.runMigrationsAndValidate( dbName, - 11, + 12, true, DashDatabase.MIGRATION_1_2, DashDatabase.MIGRATION_2_3, @@ -506,6 +515,7 @@ class DashDatabaseMigrationTest { DashDatabase.MIGRATION_8_9, DashDatabase.MIGRATION_9_10, DashDatabase.MIGRATION_10_11, + DashDatabase.MIGRATION_11_12, ).close() } } diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt index 1426fc60dea..4fa795176ff 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt @@ -502,7 +502,7 @@ abstract class NativePersistenceBridge { // ── Identity keys ───────────────────────────────────────────────── - /** One `IdentityKeyEntryFFI` upsert. Descriptor `([B[BIBBBZZJ[B[BZ[BZIIB[BLjava/lang/String;)I`. */ + /** One `IdentityKeyEntryFFI` upsert. Descriptor `([B[BIBBBZZJ[B[BZ[BZIIB[BLjava/lang/String;[B)I`. */ @Suppress("LongParameterList") open fun onPersistIdentityKeyUpsert( walletId: ByteArray, @@ -524,6 +524,7 @@ abstract class NativePersistenceBridge { contractBoundsKind: Byte, contractBoundsId: ByteArray, contractBoundsDocumentType: String?, + contractBoundsScope: ByteArray = ByteArray(0), ): Int = 0 /** One `(identityId, keyId)` removal. Descriptor `([B[BI)I`. */ @@ -1253,9 +1254,10 @@ class ContactRequestRestoreData( * `keyType` / `purpose` / `securityLevel` are DPP `repr(u8)` discriminants * (out-of-range = 255 sentinel → Rust drops the row rather than coercing to * MASTER/AUTHENTICATION, matching the Swift loader's `UInt8.max` fallback). - * `contractBoundsKind`: 0 none, 1 SingleContract, 2 SingleContractDocumentType; - * `contractBoundsId` is 32 bytes (or empty for kind 0); + * `contractBoundsKind`: 0 none, 1 SingleContract, 2 SingleContractDocumentType, 3 Scoped; + * `contractBoundsId` is 32 bytes (or empty for kinds 0 and 3); * `contractBoundsDocumentType` is non-null only for kind 2. + * Kind 3 carries the complete versioned DPP bytes in `contractBoundsScope`. */ class IdentityKeyRestoreData( @JvmField val keyId: Int, @@ -1267,6 +1269,7 @@ class IdentityKeyRestoreData( @JvmField val contractBoundsKind: Byte, @JvmField val contractBoundsId: ByteArray, @JvmField val contractBoundsDocumentType: String?, + @JvmField val contractBoundsScope: ByteArray = ByteArray(0), ) /** Mirror of `ShieldedNoteRestoreFFI`. */ diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/TransactionsNative.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/TransactionsNative.kt index d41c25b7507..b65b39139fb 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/TransactionsNative.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/TransactionsNative.kt @@ -27,8 +27,9 @@ internal object TransactionsNative { * @param addPubkeysBlob big-endian rows for the keys to add: `u32 * rowCount` then per row `u32 keyId, u8 keyType, u8 purpose, u8 * securityLevel, u8 readOnly, u8 contractBoundsKind, u16 pubkeyLen, - * pubkey`, plus (when `contractBoundsKind != 0`) a 32-byte contract id - * and (when `== 2`) `u16 docTypeLen, docType`. May be empty. + * pubkey`, plus (for kinds 1 and 2) a 32-byte contract id + * and (when `== 2`) `u16 docTypeLen, docType`; kind 3 instead carries + * `u16 scopeLen, scopeBytes`. May be empty. * @param disablePublicKeyIds key ids to disable; may be empty. At least * one of add / disable must be non-empty. */ diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityPubkeyCodec.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityPubkeyCodec.kt index 551794c731d..be2a263f8f5 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityPubkeyCodec.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityPubkeyCodec.kt @@ -28,13 +28,15 @@ import java.io.DataOutputStream * u8 purpose (DPP Purpose discriminant, 0 = AUTHENTICATION) * u8 securityLevel (DPP SecurityLevel discriminant, 0 = MASTER) * u8 readOnly (0 / 1) - * u8 contractBoundsKind (0 none, 1 SingleContract, 2 SingleContractDocumentType) + * u8 contractBoundsKind (0 none, 1 SingleContract, 2 SingleContractDocumentType, 3 Scoped) * u16 pubkeyLen * u8[pubkeyLen] pubkeyBytes (compressed pubkey, or 20-byte HASH160) - * if contractBoundsKind != 0: + * if contractBoundsKind == 1 or contractBoundsKind == 2: * u8[32] contractBoundsId * if contractBoundsKind == 2: * u16 docTypeLen, u8[docTypeLen] docType (UTF-8) + * if contractBoundsKind == 3: + * u16 scopeLen, u8[scopeLen] versioned DPP scope bytes * ``` */ object IdentityPubkeyCodec { @@ -57,6 +59,11 @@ object IdentityPubkeyCodec { dos.writeShort(k.pubkeyBytes.size) dos.write(k.pubkeyBytes) when (val bounds = k.contractBounds) { + is ContractBounds.Scoped -> { + require(bounds.encodedScope.size in 1..2048) { "Invalid scope size" } + dos.writeShort(bounds.encodedScope.size) + dos.write(bounds.encodedScope) + } null -> Unit is ContractBounds.SingleContract -> dos.write(bounds.contractId) is ContractBounds.SingleContractDocumentType -> { @@ -71,8 +78,9 @@ object IdentityPubkeyCodec { return out.toByteArray() } - /** Discriminant matching the FFI: 0 none, 1 SingleContract, 2 with doc type. */ + /** Discriminant matching the FFI: 0 none, 1 SingleContract, 2 with doc type, 3 Scoped. */ internal fun contractBoundsKind(bounds: ContractBounds?): Int = when (bounds) { + is ContractBounds.Scoped -> 3 null -> 0 is ContractBounds.SingleContract -> 1 is ContractBounds.SingleContractDocumentType -> 2 diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityUpdates.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityUpdates.kt index d9435126d55..853514273f2 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityUpdates.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityUpdates.kt @@ -47,11 +47,17 @@ enum class SecurityLevel(val ffiValue: Int) { } /** - * Contract-bounds shape for an ENCRYPTION / DECRYPTION key — Kotlin mirror - * of Swift's `ManagedPlatformWallet.ContractBounds`. Required by Drive for - * those purposes; omitted (null) for AUTHENTICATION / TRANSFER. + * Kotlin mirror of Swift's `ManagedPlatformWallet.ContractBounds`. + * Legacy variants describe encryption bounds; Scoped carries authentication grants. */ sealed class ContractBounds { + /** Versioned scope bytes produced by DPP. Rust validates them on registration. */ + data class Scoped(val encodedScope: ByteArray) : ContractBounds() { + override fun equals(other: Any?): Boolean = + other is Scoped && encodedScope.contentEquals(other.encodedScope) + override fun hashCode(): Int = encodedScope.contentHashCode() + } + /** Bind the key to a single contract (any of its document types). */ data class SingleContract(val contractId: ByteArray) : ContractBounds() { init { diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabase.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabase.kt index 822c08a242a..cf240bc091c 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabase.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabase.kt @@ -142,9 +142,11 @@ import org.dashfoundation.dashsdk.persistence.entities.WalletManagerMetadataEnti * pre-migration row reads back as an ordinary, unstamped, non-tombstone * entry, and a wallet with no recorded chainlock height has no boundary * at all (nothing collects). + * + * Version 12 (scoped authentication): preserves encoded contract scopes on public keys. */ @Database( - version = 11, + version = 12, exportSchema = true, entities = [ WalletEntity::class, @@ -542,6 +544,13 @@ abstract class DashDatabase : RoomDatabase() { } } + /** v11 → v12: preserve versioned authentication scope bytes. */ + val MIGRATION_11_12: Migration = object : Migration(11, 12) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE public_keys ADD COLUMN contractBoundsScope BLOB") + } + } + /** v9 → v10: additive DPNS marketplace state on legacy label rows. */ val MIGRATION_9_10: Migration = object : Migration(9, 10) { override fun migrate(db: SupportSQLiteDatabase) { @@ -635,6 +644,7 @@ abstract class DashDatabase : RoomDatabase() { MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, + MIGRATION_11_12, ) .build() diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt index 88a080308a0..36133ce0600 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt @@ -1863,7 +1863,12 @@ class PlatformWalletPersistenceHandler( contractBoundsKind: Byte, contractBoundsId: ByteArray, contractBoundsDocumentType: String?, + contractBoundsScope: ByteArray, ): Int = guarded { + val boundsKind = contractBoundsKind.toInt() and 0xFF + require(boundsKind in 0..3) { "Unknown contract bounds kind: $boundsKind" } + require(boundsKind != 3 || contractBoundsScope.isNotEmpty()) { "Missing authentication scope" } + // Item 1 — private-key persistence (the CLAUDE.md "one allowed // exception" shape). The `IdentityKeyEntryFFI` payload carries only // a derivation breadcrumb (`wallet_id` + `identity_index` + @@ -1978,9 +1983,9 @@ class PlatformWalletPersistenceHandler( val existing = db.publicKeyDao().getByIdentityAndKeyId(identityBase58, keyId) // ContractBounds projection → the legacy JSON blob column + // doc-type name (Swift stores `[base64(contractId)]` JSON). - val boundsData = if ((contractBoundsKind.toInt() and 0xFF) != 0) + val boundsData = if (boundsKind in 1..2) contractBoundsIdToJson(contractBoundsId) else null - val docTypeName = if ((contractBoundsKind.toInt() and 0xFF) == 2) + val docTypeName = if (boundsKind == 2) contractBoundsDocumentType else null val row = PublicKeyEntity( id = existing?.id ?: 0, @@ -1993,6 +1998,7 @@ class PlatformWalletPersistenceHandler( publicKeyData = publicKeyData, contractBoundsData = boundsData, contractBoundsDocumentTypeName = docTypeName, + contractBoundsScope = if (boundsKind == 3) contractBoundsScope.copyOf() else null, // Set to the Keystore identifier when the deriver stored the // scalar; otherwise preserve any prior identifier (idempotent // re-persist) and fall back to watch-only (null) for @@ -2857,6 +2863,7 @@ class PlatformWalletPersistenceHandler( // kind 0 rather than crashing FFI marshalling. val boundsId = pk.contractBoundsData?.let { contractBoundsJsonToId(it) } val (kind, id) = when { + pk.contractBoundsScope != null -> 3.toByte() to ByteArray(0) boundsId == null -> 0.toByte() to ByteArray(0) pk.contractBoundsDocumentTypeName != null -> 2.toByte() to boundsId else -> 1.toByte() to boundsId @@ -2869,6 +2876,7 @@ class PlatformWalletPersistenceHandler( readOnly = pk.readOnly, data = pk.publicKeyData, contractBoundsKind = kind, + contractBoundsScope = pk.contractBoundsScope ?: ByteArray(0), contractBoundsId = id, contractBoundsDocumentType = if (kind.toInt() == 2) pk.contractBoundsDocumentTypeName else null, diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/entities/PublicKeyEntity.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/entities/PublicKeyEntity.kt index 8ac11a37624..9c286567775 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/entities/PublicKeyEntity.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/entities/PublicKeyEntity.kt @@ -56,6 +56,9 @@ data class PublicKeyEntity( val contractBoundsData: ByteArray? = null, /** Document-type qualifier for `.singleContractDocumentType` bounds. */ val contractBoundsDocumentTypeName: String? = null, + + /** Versioned DPP authentication scope; null for legacy bounds. */ + val contractBoundsScope: ByteArray? = null, val privateKeyKeychainIdentifier: String? = null, /** * Derivation breadcrumb (DIP-9 identity index) captured from the diff --git a/packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseTest.kt b/packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseTest.kt index 524f6d057e8..3018c163edb 100644 --- a/packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseTest.kt +++ b/packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseTest.kt @@ -233,12 +233,12 @@ class DashDatabaseTest { } @Test - fun schemaIsAtVersion11WithTheSweepHoldIndexes() = runTest { + fun schemaIsAtVersion12WithTheSweepHoldIndexes() = runTest { // The sweep-hold columns land in ONE migration (10 → 11), with the // two `pending_inputs` indexes the sweep's claimed-row lookup // (`spendingTxid`) and the end-of-round collector // (`walletId, isSweptTombstone, winnerMinedHeight`) rely on. - assertEquals(11, db.openHelper.readableDatabase.version) + assertEquals(12, db.openHelper.readableDatabase.version) val indexes = mutableSetOf() db.openHelper.readableDatabase.query("PRAGMA index_list('pending_inputs')").use { c -> val nameColumn = c.getColumnIndexOrThrow("name") diff --git a/packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandlerTest.kt b/packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandlerTest.kt index 0aaacf099e1..c2c5ce5a105 100644 --- a/packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandlerTest.kt +++ b/packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandlerTest.kt @@ -4529,6 +4529,70 @@ class PlatformWalletPersistenceHandlerTest { assertEquals("contactRequest", key.contractBoundsDocumentType) } + @Test + fun loadWalletListPreservesScopedAuthenticationBytes() = runTest { + // Signing-critical restore path: a cold-started wallet must get its + // identities and public keys back exactly as persisted — keyId, + // repr(u8) discriminants, key bytes, and the (kind, id, docType) + // contract-bounds triple (kind 2 = SingleContractDocumentType). + handler.onPersistWalletMetadata(walletId, testnet, groupId, 0) + val xpub = ByteArray(78) { 30 } + handler.onPersistAccountRegistration( + walletId, 0, 0, 0, 0, 0, ByteArray(0), ByteArray(0), xpub, + ) + val identityId = ByteArray(32) { 12 } + seedIdentity(identityId) + val pubkey = ByteArray(33) { 7 } + val boundsId = ByteArray(0) + val scope = byteArrayOf(0, 1, 3, 0, 65, 1, 0) + + fun persistKey(kind: Byte, scopeBytes: ByteArray) = handler.onPersistIdentityKeyUpsert( + walletId = walletId, + identityId = identityId, + keyId = 4, + purpose = 0, + securityLevel = 2, + keyType = 0, + readOnly = true, + disabledAtIsSome = false, + disabledAt = 0, + publicKeyData = pubkey, + publicKeyHash = ByteArray(20), + walletIdIsSome = true, + keyWalletId = walletId, + derivationIndicesIsSome = false, + identityIndex = 0, + keyIndex = 0, + contractBoundsKind = kind, + contractBoundsId = boundsId, + contractBoundsDocumentType = null, + contractBoundsScope = scopeBytes, + ) + assertEquals(1, persistKey(4, scope)) + assertEquals(1, persistKey(3, ByteArray(0))) + handler.onChangesetBegin(walletId) + assertEquals(0, persistKey(3, scope)) + handler.onChangesetEnd(walletId, success = true) + + val list = handler.onLoadWalletList() + assertEquals(1, list.size) + assertEquals(1, list[0].identities.size) + val identity = list[0].identities[0] + assertTrue(identityId.contentEquals(identity.identityId)) + assertEquals(1, identity.keys.size) + val key = identity.keys[0] + assertEquals(4, key.keyId) + assertEquals(0.toByte(), key.keyType) + assertEquals(0.toByte(), key.purpose) + assertEquals(2.toByte(), key.securityLevel) + assertTrue(key.readOnly) + assertTrue(pubkey.contentEquals(key.data)) + assertEquals(3.toByte(), key.contractBoundsKind) + assertTrue(boundsId.contentEquals(key.contractBoundsId)) + assertNull(key.contractBoundsDocumentType) + assertTrue(scope.contentEquals(key.contractBoundsScope)) + } + // ── DashPay contacts: upsert metadata, ignore delta, restore ────── /** Persist one incoming contact row for [senderId] owned by [ownerId]. */ @@ -6018,7 +6082,7 @@ class PlatformWalletPersistenceHandlerTest { // A tombstone with a NULL stamp is never collected. The // mempool-context sweep path writes exactly this shape — an // IS-locked, unmined winner has no finality horizon to stamp — - // and legacy rows (the v10 → v11 migration leaves pre-existing + // and legacy rows (the v11 → v12 migration leaves pre-existing // tombstones NULL) read identically. With no proof of finality // the safe reading is to hold it forever rather than guess it // collectible. diff --git a/packages/rs-platform-wallet-ffi/src/identity_persistence.rs b/packages/rs-platform-wallet-ffi/src/identity_persistence.rs index fbc89290aa7..14f8b8716d4 100644 --- a/packages/rs-platform-wallet-ffi/src/identity_persistence.rs +++ b/packages/rs-platform-wallet-ffi/src/identity_persistence.rs @@ -281,6 +281,9 @@ pub struct IdentityKeyEntryFFI { // C-string are meaningful. Doc-type string is released by // [`free_identity_key_entry_ffi`]. // + // * `contract_bounds_kind == 3` — versioned AuthenticationScope bytes + // owned by `contract_bounds_scope`, released by the same free helper. + // // Keeping the kind tag inline (vs. always nulling fields) lets // the Swift side switch on a single discriminant without // probing pointer values, matching how the rest of this struct @@ -298,6 +301,10 @@ pub struct IdentityKeyEntryFFI { pub contract_bounds_kind: u8, pub contract_bounds_id: [u8; 32], pub contract_bounds_document_type: *const c_char, + /// Versioned AuthenticationScope bincode bytes for kind 3; null otherwise. + /// Ownership matches the other buffers in this struct. + pub contract_bounds_scope: *const u8, + pub contract_bounds_scope_len: usize, } /// Composite identifier for [`IdentityKeysChangeSet::removed`] entries @@ -345,8 +352,10 @@ pub struct IdentityKeyRemovalFFI { // 169..=175 (padding to 8 for pointer alignment) // 176..=183 contract_bounds_document_type *const c_char // -// Total size = 184, alignment = 8 (from u64 / pointer). -const _: [u8; 184] = [0u8; std::mem::size_of::()]; +// 184..=191 contract_bounds_scope *const u8 +// 192..=199 contract_bounds_scope_len usize +// Total size = 200, alignment = 8 (from u64 / pointer). +const _: [u8; 200] = [0u8; std::mem::size_of::()]; const _: [u8; 8] = [0u8; std::mem::align_of::()]; // Compile-time guard for `IdentityEntryFFI`. Same rationale as the @@ -652,41 +661,11 @@ impl IdentityKeyEntryFFI { /// caller owns the heap-allocated `public_key_data_ptr` byte /// buffer and (when present) the /// `contract_bounds_document_type` C-string; release both via - /// [`free_identity_key_entry_ffi`]. Scoped keys are rejected before any - /// allocation because this ABI cannot preserve their authorization bounds. - pub fn from_entry(entry: &IdentityKeyEntry) -> Result { + /// [`free_identity_key_entry_ffi`]. + pub fn from_entry(entry: &IdentityKeyEntry) -> Self { use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dpp::identity::identity_public_key::contract_bounds::ContractBounds; - // Project the DPP `ContractBounds` enum into the kind / - // id / doc-type-cstring trio so the Swift side can switch - // on a single discriminant. Strings containing interior - // NULs (impossible in practice — DPP rejects them) keep - // the discriminant + payload self-consistent by falling - // back to `SingleContract { id }` (kind=1 + null doc-type - // pointer); emitting kind=2 with a null doc-type pointer - // would silently strip the bound on the Swift side, so - // demoting to `SingleContract` is the closest faithful - // representation — the document-type qualifier is the - // only thing lost, the contract id is preserved. - let (contract_bounds_kind, contract_bounds_id, contract_bounds_document_type) = match entry - .public_key - .contract_bounds() - { - Some(ContractBounds::SingleContract { id }) => (1u8, id.to_buffer(), ptr::null()), - Some(ContractBounds::SingleContractDocumentType { - id, - document_type_name, - }) => match CString::new(document_type_name.as_str()) { - Ok(c) => (2u8, id.to_buffer(), c.into_raw() as *const c_char), - Err(_) => (1u8, id.to_buffer(), ptr::null()), - }, - Some(ContractBounds::Scoped(_)) => { - return Err("scoped authentication keys require a newer native persistence ABI"); - } - None => (0u8, [0u8; 32], ptr::null()), - }; - let pk_bytes = entry.public_key.data().as_slice().to_vec(); let pk_len = pk_bytes.len(); let pk_boxed = pk_bytes.into_boxed_slice(); @@ -708,7 +687,45 @@ impl IdentityKeyEntryFFI { None => (false, 0, 0), }; - Ok(Self { + // Project the DPP `ContractBounds` enum into the kind / + // id / doc-type-cstring trio so the Swift side can switch + // on a single discriminant. Strings containing interior + // NULs (impossible in practice — DPP rejects them) keep + // the discriminant + payload self-consistent by falling + // back to `SingleContract { id }` (kind=1 + null doc-type + // pointer); emitting kind=2 with a null doc-type pointer + // would silently strip the bound on the Swift side, so + // demoting to `SingleContract` is the closest faithful + // representation — the document-type qualifier is the + // only thing lost, the contract id is preserved. + let scope_bytes = match entry.public_key.contract_bounds() { + Some(ContractBounds::Scoped(scope)) => { + bincode::encode_to_vec(scope, bincode::config::standard()) + .expect("AuthenticationScope encoding into a Vec is infallible") + } + _ => Vec::new(), + }; + let contract_bounds_scope_len = scope_bytes.len(); + let contract_bounds_scope = if scope_bytes.is_empty() { + ptr::null() + } else { + Box::into_raw(scope_bytes.into_boxed_slice()) as *const u8 + }; + let (contract_bounds_kind, contract_bounds_id, contract_bounds_document_type) = + match entry.public_key.contract_bounds() { + Some(ContractBounds::SingleContract { id }) => (1u8, id.to_buffer(), ptr::null()), + Some(ContractBounds::SingleContractDocumentType { + id, + document_type_name, + }) => match CString::new(document_type_name.as_str()) { + Ok(c) => (2u8, id.to_buffer(), c.into_raw() as *const c_char), + Err(_) => (1u8, id.to_buffer(), ptr::null()), + }, + Some(ContractBounds::Scoped(_)) => (3u8, [0u8; 32], ptr::null()), + None => (0u8, [0u8; 32], ptr::null()), + }; + + Self { identity_id: entry.identity_id.to_buffer(), key_id: entry.key_id, purpose: entry.public_key.purpose() as u8, @@ -728,7 +745,9 @@ impl IdentityKeyEntryFFI { contract_bounds_kind, contract_bounds_id, contract_bounds_document_type, - }) + contract_bounds_scope, + contract_bounds_scope_len, + } } } @@ -874,6 +893,15 @@ unsafe fn free_optional_c_string(slot: &mut *const c_char) { /// `entry` must have been produced by /// [`IdentityKeyEntryFFI::from_entry`] and not previously freed. pub unsafe fn free_identity_key_entry_ffi(entry: &mut IdentityKeyEntryFFI) { + if !entry.contract_bounds_scope.is_null() { + let raw = std::ptr::slice_from_raw_parts_mut( + entry.contract_bounds_scope as *mut u8, + entry.contract_bounds_scope_len, + ); + drop(unsafe { Box::from_raw(raw) }); + entry.contract_bounds_scope = ptr::null(); + entry.contract_bounds_scope_len = 0; + } if !entry.public_key_data_ptr.is_null() && entry.public_key_data_len > 0 { // Reconstruct the boxed slice we created via `Box::into_raw` // on a `Box<[u8]>`. Using `Vec::from_raw_parts` would over- @@ -1193,7 +1221,7 @@ mod tests { key_index: 5, }), }; - let mut ffi = IdentityKeyEntryFFI::from_entry(&entry).unwrap(); + let mut ffi = IdentityKeyEntryFFI::from_entry(&entry); assert_eq!(ffi.identity_id, [2u8; 32]); assert_eq!(ffi.key_id, 5); assert_eq!(ffi.purpose, Purpose::AUTHENTICATION as u8); @@ -1235,7 +1263,7 @@ mod tests { wallet_id: None, derivation_indices: None, }; - let mut ffi = IdentityKeyEntryFFI::from_entry(&entry).unwrap(); + let mut ffi = IdentityKeyEntryFFI::from_entry(&entry); assert!(!ffi.wallet_id_is_some); assert!(!ffi.derivation_indices_is_some); assert!(ffi.read_only); @@ -1247,37 +1275,33 @@ mod tests { } #[test] - fn scoped_keys_are_rejected_before_native_persistence_callbacks() { - use crate::persistence::{FFIPersister, PersistenceCallbacks}; + fn scoped_key_payload_survives_ffi_projection_and_registration_decode() { + use crate::identity_registration_with_signer::{decode_contract_bounds, IdentityPubkeyFFI}; use dpp::identity::contract_bounds::{ AuthenticationScope, AuthenticationScopeV0, ContractBounds, ContractScope, }; - use platform_wallet::changeset::PlatformWalletPersistence; - use platform_wallet::{IdentityKeysChangeSet, PlatformWalletChangeSet}; - use std::sync::atomic::{AtomicBool, Ordering}; - - unsafe extern "C" fn begin(context: *mut std::ffi::c_void, _: *const u8) -> i32 { - (*(context as *const AtomicBool)).store(true, Ordering::SeqCst); - 0 - } - + let scope = AuthenticationScope::V0(AuthenticationScopeV0 { + contracts: vec![ + ContractScope { + id: Identifier::from([1; 32]), + document_types: Some(vec!["post".into()]), + }, + ContractScope { + id: Identifier::from([2; 32]), + document_types: None, + }, + ], + permissions: 65, + expires_at: Some(1_900_000_000_000), + }); let entry = IdentityKeyEntry { - identity_id: Identifier::from([1; 32]), - key_id: 1, + identity_id: Identifier::from([3; 32]), + key_id: 5, public_key: IdentityPublicKey::V0(IdentityPublicKeyV0 { - id: 1, + id: 5, purpose: Purpose::AUTHENTICATION, security_level: SecurityLevel::HIGH, - contract_bounds: Some(ContractBounds::Scoped(AuthenticationScope::V0( - AuthenticationScopeV0 { - contracts: vec![ContractScope { - id: Identifier::from([2; 32]), - document_types: None, - }], - permissions: 1, - expires_at: None, - }, - ))), + contract_bounds: Some(ContractBounds::Scoped(scope.clone())), key_type: KeyType::ECDSA_SECP256K1, read_only: false, data: BinaryData::new(vec![2; 33]), @@ -1287,27 +1311,36 @@ mod tests { wallet_id: None, derivation_indices: None, }; - assert!(IdentityKeyEntryFFI::from_entry(&entry).is_err()); - - let began = AtomicBool::new(false); - let persister = FFIPersister::new(PersistenceCallbacks { - context: &began as *const AtomicBool as *mut std::ffi::c_void, - on_changeset_begin_fn: Some(begin), - ..Default::default() - }); - let changeset = PlatformWalletChangeSet { - identity_keys: Some(IdentityKeysChangeSet { - upserts: [((entry.identity_id, entry.key_id), entry)].into(), - ..Default::default() - }), - ..Default::default() + let mut ffi = IdentityKeyEntryFFI::from_entry(&entry); + assert_eq!(ffi.contract_bounds_kind, 3); + let row = IdentityPubkeyFFI { + key_id: ffi.key_id, + key_type: ffi.key_type, + purpose: ffi.purpose, + security_level: ffi.security_level, + pubkey_bytes: ffi.public_key_data_ptr, + pubkey_len: ffi.public_key_data_len, + read_only: ffi.read_only, + contract_bounds_kind: ffi.contract_bounds_kind, + contract_bounds_id: ptr::null(), + contract_bounds_document_type: ptr::null(), + contract_bounds_scope: ffi.contract_bounds_scope, + contract_bounds_scope_len: ffi.contract_bounds_scope_len, + }; + let decoded = unsafe { decode_contract_bounds(&row, Purpose::AUTHENTICATION, 0, "keys") }; + assert!(matches!(decoded, Ok(Some(ContractBounds::Scoped(value))) if value == scope)); + let invalid_row = IdentityPubkeyFFI { + contract_bounds_scope_len: row.contract_bounds_scope_len - 1, + ..row }; - assert!(persister.store([3; 32], changeset).is_err()); - assert!(!began.load(Ordering::SeqCst)); - assert!(persister - .store([3; 32], PlatformWalletChangeSet::default()) - .is_ok()); - assert!(began.load(Ordering::SeqCst)); + assert!(unsafe { + decode_contract_bounds(&invalid_row, Purpose::AUTHENTICATION, 0, "keys") + } + .is_err()); + unsafe { free_identity_key_entry_ffi(&mut ffi) }; + assert!(ffi.contract_bounds_scope.is_null()); + assert_eq!(ffi.contract_bounds_scope_len, 0); + unsafe { free_identity_key_entry_ffi(&mut ffi) }; } #[test] @@ -1332,7 +1365,7 @@ mod tests { wallet_id: None, derivation_indices: None, }; - let mut ffi = IdentityKeyEntryFFI::from_entry(&entry).unwrap(); + let mut ffi = IdentityKeyEntryFFI::from_entry(&entry); assert_eq!(ffi.contract_bounds_kind, 1); assert_eq!(ffi.contract_bounds_id, [0xAB; 32]); assert!(ffi.contract_bounds_document_type.is_null()); @@ -1364,7 +1397,7 @@ mod tests { wallet_id: None, derivation_indices: None, }; - let mut ffi = IdentityKeyEntryFFI::from_entry(&entry).unwrap(); + let mut ffi = IdentityKeyEntryFFI::from_entry(&entry); assert_eq!(ffi.contract_bounds_kind, 2); assert_eq!(ffi.contract_bounds_id, [0xCD; 32]); assert!(!ffi.contract_bounds_document_type.is_null()); diff --git a/packages/rs-platform-wallet-ffi/src/identity_registration_with_signer.rs b/packages/rs-platform-wallet-ffi/src/identity_registration_with_signer.rs index f6e58733d0f..9c2eb1ccce6 100644 --- a/packages/rs-platform-wallet-ffi/src/identity_registration_with_signer.rs +++ b/packages/rs-platform-wallet-ffi/src/identity_registration_with_signer.rs @@ -94,12 +94,10 @@ use crate::{unwrap_option_or_return, unwrap_result_or_return}; /// the caller retains ownership. Compressed secp256k1 pubkeys are /// always 33 bytes (`pubkey_len == 33`); BLS would be 48; etc. /// -/// **Contract bounds** — keys may optionally carry a reference to -/// the contract (and optionally a document type) they're allowed to -/// operate within. Consensus accepts unbounded keys for every -/// purpose, including Encryption / Decryption; bounds only become -/// meaningful when the target contract or document type explicitly -/// requires a bounded key. Encoded inline as: +/// **Contract bounds** — legacy variants qualify encryption/decryption +/// keys by contract or document type. Scoped authentication grants encode +/// the contracts, document operations and expiry that consensus enforces. +/// Encoded inline as: /// - `contract_bounds_kind == 0` → no bounds. /// - `contract_bounds_kind == 1` → `SingleContract`. The first /// 32 bytes at `contract_bounds_id` are the contract id; the @@ -108,6 +106,8 @@ use crate::{unwrap_option_or_return, unwrap_result_or_return}; /// `contract_bounds_id` is the 32-byte contract id; /// `contract_bounds_document_type` is a NUL-terminated UTF-8 /// document type name. Both must be non-null. +/// - `contract_bounds_kind == 3` → `Scoped`. The scope pointer/length +/// contain the complete versioned AuthenticationScope bincode payload. /// /// All pointers are borrowed for the call duration only — the /// FFI does not retain or free them. @@ -122,11 +122,15 @@ pub struct IdentityPubkeyFFI { pub read_only: bool, /// Discriminant for the contract-bounds union. See struct doc. pub contract_bounds_kind: u8, - /// 32-byte contract id when `contract_bounds_kind != 0`. + /// 32-byte contract id when `contract_bounds_kind` is 1 or 2. pub contract_bounds_id: *const u8, /// NUL-terminated UTF-8 document type name when /// `contract_bounds_kind == 2`. Null otherwise. pub contract_bounds_document_type: *const std::os::raw::c_char, + /// Versioned AuthenticationScope bincode bytes for kind 3; null otherwise. + /// Ownership matches the other buffers in this struct. + pub contract_bounds_scope: *const u8, + pub contract_bounds_scope_len: usize, } /// Decode the optional `contract_bounds_*` payload off an @@ -215,11 +219,33 @@ pub(crate) unsafe fn decode_contract_bounds( document_type_name: doc_type, })) } + 3 => { + if row.contract_bounds_scope.is_null() + || row.contract_bounds_scope_len == 0 + || row.contract_bounds_scope_len + > dpp::identity::contract_bounds::authentication_scope::MAX_SCOPE_BYTES + { + return Err(PlatformWalletFFIResult::err( + PlatformWalletFFIResultCode::ErrorInvalidParameter, + format!("{field_label}[{row_index}] has an invalid scope buffer"), + )); + } + let bytes = + slice::from_raw_parts(row.contract_bounds_scope, row.contract_bounds_scope_len); + dpp::identity::contract_bounds::AuthenticationScope::from_bytes(bytes) + .map(|scope| Some(ContractBounds::Scoped(scope))) + .map_err(|error| { + PlatformWalletFFIResult::err( + PlatformWalletFFIResultCode::ErrorInvalidParameter, + error.to_string(), + ) + }) + } other => Err(PlatformWalletFFIResult::err( PlatformWalletFFIResultCode::ErrorInvalidParameter, format!( "{field_label}[{row_index}].contract_bounds_kind = {other} is not a valid \ - discriminant (0=none, 1=SingleContract, 2=SingleContractDocumentType)" + discriminant (0=none, 1=SingleContract, 2=SingleContractDocumentType, 3=Scoped)" ), )), } @@ -839,6 +865,8 @@ mod tests { contract_bounds_kind: 0, contract_bounds_id: ptr::null(), contract_bounds_document_type: ptr::null(), + contract_bounds_scope: std::ptr::null(), + contract_bounds_scope_len: 0, } } diff --git a/packages/rs-platform-wallet-ffi/src/identity_update.rs b/packages/rs-platform-wallet-ffi/src/identity_update.rs index 670d9630e6b..5f780a735f2 100644 --- a/packages/rs-platform-wallet-ffi/src/identity_update.rs +++ b/packages/rs-platform-wallet-ffi/src/identity_update.rs @@ -45,10 +45,14 @@ pub struct ParsedIdentityUpdatePublicKeyFFI { pub read_only: bool, pub data_ptr: *mut u8, pub data_len: usize, - /// 0 = none, 1 = SingleContract, 2 = SingleContractDocumentType. + /// 0 = none, 1 = SingleContract, 2 = SingleContractDocumentType, 3 = Scoped. pub contract_bounds_kind: u8, pub contract_bounds_id: [u8; 32], pub contract_bounds_document_type: *mut c_char, + /// Versioned AuthenticationScope bincode bytes for kind 3; null otherwise. + /// Ownership matches the other buffers in this struct. + pub contract_bounds_scope: *const u8, + pub contract_bounds_scope_len: usize, } /// Owned C representation of the inspectable parts of a parsed @@ -119,10 +123,7 @@ fn encode_contract_bounds( ), )), }, - Some(ContractBounds::Scoped(_)) => Err(PlatformWalletFFIResult::err( - PlatformWalletFFIResultCode::ErrorInvalidParameter, - "scoped authentication keys require a newer native inspection ABI", - )), + Some(ContractBounds::Scoped(_)) => Ok((3u8, [0u8; 32], ptr::null_mut())), None => Ok((0u8, [0u8; 32], ptr::null_mut())), } } @@ -136,6 +137,14 @@ fn encode_contract_bounds( /// pointer this module allocated and has not freed yet. unsafe fn free_parsed_public_keys(keys: &mut [ParsedIdentityUpdatePublicKeyFFI]) { for key in keys.iter_mut() { + if !key.contract_bounds_scope.is_null() { + drop(Box::from_raw(ptr::slice_from_raw_parts_mut( + key.contract_bounds_scope as *mut u8, + key.contract_bounds_scope_len, + ))); + key.contract_bounds_scope = ptr::null(); + key.contract_bounds_scope_len = 0; + } if !key.data_ptr.is_null() && key.data_len > 0 { let data_slice = slice::from_raw_parts_mut(key.data_ptr, key.data_len); let _ = Box::from_raw(data_slice as *mut [u8]); @@ -173,6 +182,25 @@ pub(crate) fn project_parsed_identity_update( } }; + let scope_bytes = match public_key.contract_bounds() { + Some(ContractBounds::Scoped(scope)) => match scope.to_bytes() { + Ok(bytes) => bytes, + Err(error) => { + unsafe { free_parsed_public_keys(&mut add_public_keys_vec) }; + return Err(PlatformWalletFFIResult::err( + PlatformWalletFFIResultCode::ErrorInvalidParameter, + error.to_string(), + )); + } + }, + _ => Vec::new(), + }; + let contract_bounds_scope_len = scope_bytes.len(); + let contract_bounds_scope = if scope_bytes.is_empty() { + ptr::null() + } else { + Box::into_raw(scope_bytes.into_boxed_slice()) as *const u8 + }; let data = public_key.data().as_slice().to_vec().into_boxed_slice(); let data_len = data.len(); let data_ptr = Box::into_raw(data) as *mut u8; @@ -188,6 +216,8 @@ pub(crate) fn project_parsed_identity_update( contract_bounds_kind, contract_bounds_id, contract_bounds_document_type, + contract_bounds_scope, + contract_bounds_scope_len, }); } diff --git a/packages/rs-platform-wallet-ffi/src/invitation.rs b/packages/rs-platform-wallet-ffi/src/invitation.rs index 721f0910b10..f5a0ab9264b 100644 --- a/packages/rs-platform-wallet-ffi/src/invitation.rs +++ b/packages/rs-platform-wallet-ffi/src/invitation.rs @@ -765,6 +765,8 @@ mod tests { contract_bounds_kind: 0, contract_bounds_id: std::ptr::null(), contract_bounds_document_type: std::ptr::null(), + contract_bounds_scope: std::ptr::null(), + contract_bounds_scope_len: 0, }; let rows = [ffi_row(&pk_a), ffi_row(&pk_b)]; let dummy_signer = std::ptr::dangling_mut::(); diff --git a/packages/rs-platform-wallet-ffi/src/managed_identity.rs b/packages/rs-platform-wallet-ffi/src/managed_identity.rs index 71e7c7544ee..9a5e0a29732 100644 --- a/packages/rs-platform-wallet-ffi/src/managed_identity.rs +++ b/packages/rs-platform-wallet-ffi/src/managed_identity.rs @@ -186,6 +186,8 @@ pub struct IdentityPublicKeyFFI { pub disabled_at: u64, pub data_ptr: *mut u8, pub data_len: usize, + /// Complete DPP bounds JSON, null for an unrestricted key. Rust-owned. + pub contract_bounds_json: *mut std::os::raw::c_char, } /// Snapshot every `IdentityPublicKey` on the identity into a flat @@ -221,6 +223,14 @@ pub unsafe extern "C" fn managed_identity_get_public_keys( None => (false, 0u64), }; + let contract_bounds_json = + pk.contract_bounds().map_or(std::ptr::null_mut(), |bounds| { + let json = serde_json::to_string(bounds) + .expect("ContractBounds JSON serialization is infallible"); + std::ffi::CString::new(json) + .expect("JSON escapes NUL bytes") + .into_raw() + }); buf.push(IdentityPublicKeyFFI { key_id, purpose: pk.purpose() as u8, @@ -231,6 +241,7 @@ pub unsafe extern "C" fn managed_identity_get_public_keys( disabled_at: disabled_val, data_ptr, data_len, + contract_bounds_json, }); } buf @@ -263,6 +274,10 @@ pub unsafe extern "C" fn managed_identity_free_public_keys( } let slice = unsafe { std::slice::from_raw_parts_mut(keys, count) }; for entry in slice.iter_mut() { + if !entry.contract_bounds_json.is_null() { + drop(unsafe { std::ffi::CString::from_raw(entry.contract_bounds_json) }); + entry.contract_bounds_json = std::ptr::null_mut(); + } if !entry.data_ptr.is_null() && entry.data_len > 0 { let data_slice = unsafe { std::slice::from_raw_parts_mut(entry.data_ptr, entry.data_len) }; @@ -326,6 +341,40 @@ mod tests { Identity::V0(identity_v0) } + #[test] + fn scoped_public_key_snapshot_keeps_bounds_json() { + use dpp::identity::contract_bounds::{ + AuthenticationScope, AuthenticationScopeV0, ContractBounds, ContractScope, + }; + let scope = ContractBounds::Scoped(AuthenticationScope::V0(AuthenticationScopeV0 { + contracts: vec![ContractScope { + id: Identifier::from([7; 32]), + document_types: None, + }], + permissions: 65, + expires_at: Some(1_900_000_000_000), + })); + let mut identity = create_test_identity(); + let Identity::V0(inner) = &mut identity; + let IdentityPublicKey::V0(key) = inner.public_keys.get_mut(&0).unwrap(); + key.security_level = SecurityLevel::HIGH; + key.contract_bounds = Some(scope.clone()); + let handle = MANAGED_IDENTITY_STORAGE.insert(ManagedIdentity::new(identity, 0)); + let mut keys = std::ptr::null_mut(); + let mut count = 0; + unsafe { + let result = managed_identity_get_public_keys(handle, &mut keys, &mut count); + assert_eq!(result.code, PlatformWalletFFIResultCode::Success); + assert_eq!(count, 1); + let json = std::ffi::CStr::from_ptr((*keys).contract_bounds_json) + .to_str() + .unwrap(); + assert_eq!(serde_json::from_str::(json).unwrap(), scope); + managed_identity_free_public_keys(keys, count); + managed_identity_destroy(handle); + } + } + #[test] fn test_get_and_set_label_stub_returns_null() { unsafe { diff --git a/packages/rs-platform-wallet-ffi/src/persistence.rs b/packages/rs-platform-wallet-ffi/src/persistence.rs index 4dae5e6fe4c..bdcbd75aff0 100644 --- a/packages/rs-platform-wallet-ffi/src/persistence.rs +++ b/packages/rs-platform-wallet-ffi/src/persistence.rs @@ -1694,23 +1694,6 @@ impl PlatformWalletPersistence for FFIPersister { wallet_id: WalletId, changeset: PlatformWalletChangeSet, ) -> Result<(), PersistenceError> { - // The legacy native ABI cannot represent Scoped. Reject the whole - // round before any callback; never persist an unrestricted projection. - if let Some(keys) = &changeset.identity_keys { - use dpp::identity::contract_bounds::ContractBounds; - use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; - if keys.upserts.values().any(|entry| { - matches!( - entry.public_key.contract_bounds(), - Some(ContractBounds::Scoped(_)) - ) - }) { - return Err(PersistenceError::backend( - "scoped authentication keys require a newer native persistence ABI", - )); - } - } - // Serialize the ENTIRE begin→per-kind→end round against every // other round producer (see `round_lock`'s field doc and // dashpay/platform#4069). The lock is a synchronous @@ -2148,10 +2131,11 @@ impl PlatformWalletPersistence for FFIPersister { // `PersistentPublicKey` rows. if let Some(ref keys_cs) = changeset.identity_keys { if let Some(cb) = self.callbacks.on_persist_identity_keys_fn { - let mut upserts = Vec::with_capacity(keys_cs.upserts.len()); - let projection = keys_cs.upserts.values().try_for_each(|entry| { - IdentityKeyEntryFFI::from_entry(entry).map(|entry| upserts.push(entry)) - }); + let mut upserts: Vec = keys_cs + .upserts + .values() + .map(IdentityKeyEntryFFI::from_entry) + .collect(); let removed: Vec = keys_cs .removed .iter() @@ -2160,25 +2144,19 @@ impl PlatformWalletPersistence for FFIPersister { key_id: *key_id, }) .collect(); - let result = if projection.is_ok() { - unsafe { - cb( - self.callbacks.context, - wallet_id.as_ptr(), - upserts.as_ptr(), - upserts.len(), - if removed.is_empty() { - std::ptr::null() - } else { - removed.as_ptr() - }, - removed.len(), - ) - } - } else { - // Preserve rollback and free every successful projection - // on any projection failure added in the future. - -1 + let result = unsafe { + cb( + self.callbacks.context, + wallet_id.as_ptr(), + upserts.as_ptr(), + upserts.len(), + if removed.is_empty() { + std::ptr::null() + } else { + removed.as_ptr() + }, + removed.len(), + ) }; for entry in upserts.iter_mut() { unsafe { free_identity_key_entry_ffi(entry) }; @@ -6177,8 +6155,8 @@ unsafe fn build_identity_public_keys( // inconsistency (the writer is supposed to demote to // kind=1 in that case — see identity_persistence.rs); we // demote it here too rather than fabricating an empty doc- - // type name. Invalid kind tags load as unbounded so a - // forward-compatible writer doesn't lock us out. + // type name. Unknown kinds and corrupt scoped payloads are skipped + // with a warning; they must never become unbounded keys. let contract_bounds: Option = match row.contract_bounds_kind { 0 => None, 1 => Some(ContractBounds::SingleContract { @@ -6201,7 +6179,35 @@ unsafe fn build_identity_public_keys( } } } - _ => None, + 3 => { + if row.contract_bounds_scope.is_null() + || row.contract_bounds_scope_len == 0 + || row.contract_bounds_scope_len + > dpp::identity::contract_bounds::authentication_scope::MAX_SCOPE_BYTES + { + tracing::warn!( + key_id = row.key_id, + "Skipping key with invalid persisted scope buffer" + ); + continue; + } + match dpp::identity::contract_bounds::AuthenticationScope::from_bytes( + slice::from_raw_parts(row.contract_bounds_scope, row.contract_bounds_scope_len), + ) { + Ok(scope) => Some(ContractBounds::Scoped(scope)), + Err(error) => { + tracing::warn!(key_id = row.key_id, %error, "Skipping key with corrupt persisted scope"); + continue; + } + } + } + _ => { + tracing::warn!( + key_id = row.key_id, + "Skipping key with unknown persisted bounds kind" + ); + continue; + } }; let pk = IdentityPublicKey::V0(IdentityPublicKeyV0 { @@ -9849,3 +9855,56 @@ mod tests { ); } } + +#[cfg(test)] +mod scoped_key_restore_tests { + use super::*; + use dpp::identity::contract_bounds::{ + AuthenticationScope, AuthenticationScopeV0, ContractBounds, ContractScope, + }; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use std::ptr; + + #[test] + fn restore_retains_scope_and_never_widens_corrupt_scope() { + let scope = AuthenticationScope::V0(AuthenticationScopeV0 { + contracts: vec![ContractScope { + id: Identifier::from([7; 32]), + document_types: None, + }], + permissions: 65, + expires_at: Some(1_900_000_000_000), + }); + let bytes = scope.to_bytes().unwrap(); + let key_data = [2; 33]; + let mut key = IdentityKeyRestoreFFI { + key_id: 5, + key_type: 0, + purpose: 0, + security_level: 2, + read_only: false, + data: key_data.as_ptr(), + data_len: key_data.len(), + contract_bounds_kind: 3, + contract_bounds_id: [0; 32], + contract_bounds_document_type: ptr::null(), + contract_bounds_scope: bytes.as_ptr(), + contract_bounds_scope_len: bytes.len(), + }; + // The repr(C) restore envelope consists exclusively of integer and raw-pointer fields. + let mut spec: IdentityRestoreEntryFFI = unsafe { std::mem::zeroed() }; + spec.keys = &key; + spec.keys_count = 1; + let restored = unsafe { build_identity_public_keys(&spec) }; + assert_eq!( + restored[&5].contract_bounds(), + Some(&ContractBounds::Scoped(scope)) + ); + key.contract_bounds_scope_len -= 1; + spec.keys = &key; + assert!(unsafe { build_identity_public_keys(&spec) }.is_empty()); + key.contract_bounds_kind = 255; + spec.keys = &key; + assert!(unsafe { build_identity_public_keys(&spec) }.is_empty()); + } +} diff --git a/packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs b/packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs index c49be7de1b7..51d1abdac45 100644 --- a/packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs +++ b/packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs @@ -192,7 +192,7 @@ pub struct AccountSpecFFI { /// /// `contract_bounds_*` mirror the [`IdentityKeyEntryFFI`] /// projection of DPP's `ContractBounds` enum (kind tag: 0=none, -/// 1=SingleContract, 2=SingleContractDocumentType). Including them +/// 1=SingleContract, 2=SingleContractDocumentType, 3=Scoped). Including them /// here closes the persist↔restore round-trip — without it, scoped /// DashPay keys (registered with `SingleContractDocumentType`) come /// back as unbounded on cold restart. @@ -212,11 +212,11 @@ pub struct IdentityKeyRestoreFFI { pub data: *const u8, pub data_len: usize, /// ContractBounds discriminant: 0=none, 1=SingleContract, - /// 2=SingleContractDocumentType. Mirrors the encoding in + /// 2=SingleContractDocumentType, 3=Scoped. Mirrors the encoding in /// [`crate::identity_persistence::IdentityKeyEntryFFI`]. pub contract_bounds_kind: u8, /// 32-byte contract identifier. Zeroed when - /// `contract_bounds_kind == 0`; otherwise the contract id the + /// `contract_bounds_kind` is 0 or 3; otherwise the contract id the /// key is bound to. pub contract_bounds_id: [u8; 32], /// NUL-terminated UTF-8 doc-type name. Non-null iff @@ -224,6 +224,10 @@ pub struct IdentityKeyRestoreFFI { /// same load-callback allocation arena that frees the public- /// key data buffer). pub contract_bounds_document_type: *const c_char, + /// Versioned AuthenticationScope bincode bytes for kind 3; null otherwise. + /// Ownership matches the other buffers in this struct. + pub contract_bounds_scope: *const u8, + pub contract_bounds_scope_len: usize, } /// Per-identity entry attached to a [`WalletRestoreEntryFFI`]. diff --git a/packages/rs-sdk-ffi/src/identity/mod.rs b/packages/rs-sdk-ffi/src/identity/mod.rs index aae14eab159..8d956023c14 100644 --- a/packages/rs-sdk-ffi/src/identity/mod.rs +++ b/packages/rs-sdk-ffi/src/identity/mod.rs @@ -31,7 +31,7 @@ pub use keys::{ dash_sdk_identity_public_key_destroy, dash_sdk_identity_public_key_get_id, StateTransitionType, }; pub use names::dash_sdk_identity_register_name; -pub use parse::dash_sdk_identity_parse_json; +pub use parse::{dash_sdk_contract_bounds_parse_json, dash_sdk_identity_parse_json}; pub use put::{ dash_sdk_identity_put_to_platform_with_chain_lock, dash_sdk_identity_put_to_platform_with_chain_lock_and_wait, diff --git a/packages/rs-sdk-ffi/src/identity/parse.rs b/packages/rs-sdk-ffi/src/identity/parse.rs index 342c29b6d18..1e9aefd2f20 100644 --- a/packages/rs-sdk-ffi/src/identity/parse.rs +++ b/packages/rs-sdk-ffi/src/identity/parse.rs @@ -81,3 +81,86 @@ pub unsafe extern "C" fn dash_sdk_identity_parse_json(json_str: *const c_char) - )), } } + +/// Normalize DPP contract-bounds JSON for native persistence. Scope encoding +/// stays in Rust so host clients never reconstruct the consensus wire format. +/// +/// # Safety +/// `json_str` must point to a valid NUL-terminated UTF-8 string for this call. +/// Release the returned string with `dash_sdk_string_free`. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_contract_bounds_parse_json( + json_str: *const c_char, +) -> DashSDKResult { + use dash_sdk::dpp::identity::contract_bounds::ContractBounds; + let convert = || -> Result { + if json_str.is_null() { + return Err("Contract bounds JSON is null".into()); + } + let json = CStr::from_ptr(json_str) + .to_str() + .map_err(|e| e.to_string())?; + let bounds: ContractBounds = serde_json::from_str(json).map_err(|e| e.to_string())?; + let value = match bounds { + ContractBounds::SingleContract { id } => { + serde_json::json!({"kind": 1, "id": id.to_buffer().to_vec()}) + } + ContractBounds::SingleContractDocumentType { + id, + document_type_name, + } => { + serde_json::json!({"kind": 2, "id": id.to_buffer().to_vec(), "documentType": document_type_name}) + } + ContractBounds::Scoped(scope) => { + serde_json::json!({"kind": 3, "scope": scope.to_bytes().map_err(|e| e.to_string())?}) + } + }; + Ok(value.to_string()) + }; + match convert() { + Ok(json) => DashSDKResult::success_string( + std::ffi::CString::new(json) + .expect("JSON has no NUL bytes") + .into_raw(), + ), + Err(error) => DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::SerializationError, + error, + )), + } +} + +#[cfg(test)] +mod scoped_bounds_tests { + use super::*; + use dash_sdk::dpp::identity::contract_bounds::{ + AuthenticationScope, AuthenticationScopeV0, ContractBounds, ContractScope, + }; + use dash_sdk::dpp::prelude::Identifier; + + #[test] + fn scoped_json_normalization_preserves_complete_wire_payload() { + let scope = AuthenticationScope::V0(AuthenticationScopeV0 { + contracts: vec![ContractScope { + id: Identifier::from([1; 32]), + document_types: Some(vec!["post".into()]), + }], + permissions: 65, + expires_at: Some(1_900_000_000_000), + }); + let json = std::ffi::CString::new( + serde_json::to_string(&ContractBounds::Scoped(scope.clone())).unwrap(), + ) + .unwrap(); + let mut result = unsafe { dash_sdk_contract_bounds_parse_json(json.as_ptr()) }; + assert!(result.error.is_null()); + let output = unsafe { CStr::from_ptr(result.data as *const c_char) } + .to_str() + .unwrap(); + let normalized: serde_json::Value = serde_json::from_str(output).unwrap(); + assert_eq!(normalized["kind"], 3); + let bytes: Vec = serde_json::from_value(normalized["scope"].clone()).unwrap(); + assert_eq!(AuthenticationScope::from_bytes(&bytes).unwrap(), scope); + unsafe { crate::types::dash_sdk_result_free(&mut result) }; + } +} diff --git a/packages/rs-unified-sdk-jni/src/persistence.rs b/packages/rs-unified-sdk-jni/src/persistence.rs index 732534d25f0..66532808fab 100644 --- a/packages/rs-unified-sdk-jni/src/persistence.rs +++ b/packages/rs-unified-sdk-jni/src/persistence.rs @@ -1302,6 +1302,9 @@ unsafe extern "C" fn tramp_persist_identity_keys( }) } +// Shared with descriptor verification so the smoke check resolves the actual call signature. +const IDENTITY_KEY_UPSERT_DESCRIPTOR: &str = "([B[BIBBBZZJ[B[BZ[BZIIB[BLjava/lang/String;[B)I"; + unsafe fn persist_identity_key_upsert( env: &mut JNIEnv, bridge: &JObject, @@ -1314,10 +1317,11 @@ unsafe fn persist_identity_key_upsert( let key_wallet_id = env.byte_array_from_slice(&e.wallet_id)?; let cb_id = env.byte_array_from_slice(&e.contract_bounds_id)?; let cb_doctype = cstr_opt(env, e.contract_bounds_document_type)?; + let cb_scope = bytes(env, e.contract_bounds_scope, e.contract_bounds_scope_len)?; env.call_method( bridge, "onPersistIdentityKeyUpsert", - "([B[BIBBBZZJ[B[BZ[BZIIB[BLjava/lang/String;)I", + IDENTITY_KEY_UPSERT_DESCRIPTOR, &[ wid.into(), (&identity_id).into(), @@ -1338,6 +1342,7 @@ unsafe fn persist_identity_key_upsert( JValue::Byte(e.contract_bounds_kind as i8), (&cb_id).into(), (&cb_doctype).into(), + (&cb_scope).into(), ], )? .i() @@ -2137,6 +2142,7 @@ struct IdentityKeyRestoreStaged { key: IdentityKeyRestoreFFI, data: Vec, doc_type: Option, + scope: Vec, } /// Mint the raw FFI pointers for a fully staged wallet list. Infallible: @@ -2301,8 +2307,13 @@ fn seal_wallet_entries(staged: Vec) -> Vec, pub(crate) contract_bounds_id: Option<[u8; 32]>, pub(crate) contract_bounds_document_type: Option, + pub(crate) contract_bounds_scope: Vec, } impl DecodedPubkeyRow { @@ -87,6 +88,8 @@ impl DecodedPubkeyRow { pubkey_len: self.pubkey_bytes.len(), read_only: self.read_only, contract_bounds_kind: self.contract_bounds_kind, + contract_bounds_scope: self.contract_bounds_scope.as_ptr(), + contract_bounds_scope_len: self.contract_bounds_scope.len(), contract_bounds_id: self .contract_bounds_id .as_ref() @@ -110,13 +113,15 @@ impl DecodedPubkeyRow { /// u8 purpose (DPP Purpose discriminant, 0 = AUTHENTICATION) /// u8 security_level (DPP SecurityLevel discriminant, 0 = MASTER) /// u8 read_only (0 / 1 — any other byte is rejected) -/// u8 contract_bounds_kind (0 none, 1 SingleContract, 2 SingleContractDocumentType) +/// u8 contract_bounds_kind (0 none, 1 SingleContract, 2 SingleContractDocumentType, 3 Scoped) /// u16 pubkey_len /// u8[pubkey_len] pubkey_bytes (compressed pubkey, or 20-byte HASH160) -/// if contract_bounds_kind != 0: +/// if contract_bounds_kind == 1 or contract_bounds_kind == 2: /// u8[32] contract_bounds_id /// if contract_bounds_kind == 2: /// u16 doc_type_len, u8[doc_type_len] doc_type (UTF-8) +/// if contract_bounds_kind == 3: +/// u16 scope_len, u8[scope_len] versioned DPP scope bytes /// ``` /// /// Strict: returns `Err` on truncation, trailing bytes, a negative key ID @@ -180,9 +185,9 @@ pub(crate) fn parse_pubkey_rows(bytes: &[u8]) -> Result, S } }; let contract_bounds_kind = fixed[8]; - if contract_bounds_kind > 2 { + if contract_bounds_kind > 3 { return Err(format!( - "pubkey blob row {i} contractBoundsKind must be 0, 1 or 2, got {contract_bounds_kind}" + "pubkey blob row {i} contractBoundsKind must be 0, 1, 2 or 3, got {contract_bounds_kind}" )); } let pubkey_len = u16::from_be_bytes([fixed[9], fixed[10]]) as usize; @@ -192,7 +197,7 @@ pub(crate) fn parse_pubkey_rows(bytes: &[u8]) -> Result, S let mut contract_bounds_id: Option<[u8; 32]> = None; let mut contract_bounds_document_type: Option = None; - if contract_bounds_kind != 0 { + if matches!(contract_bounds_kind, 1 | 2) { let id_bytes = read(&mut cursor, 32) .ok_or_else(|| format!("pubkey blob truncated at row {i} contractBoundsId"))?; let mut id = [0u8; 32]; @@ -212,6 +217,19 @@ pub(crate) fn parse_pubkey_rows(bytes: &[u8]) -> Result, S } } + let contract_bounds_scope = if contract_bounds_kind == 3 { + let length = read(&mut cursor, 2) + .ok_or_else(|| format!("pubkey blob truncated at row {i} scope length"))?; + let length = u16::from_be_bytes([length[0], length[1]]) as usize; + if length == 0 || length > 2048 { + return Err(format!("pubkey blob row {i} invalid scope length")); + } + read(&mut cursor, length) + .ok_or_else(|| format!("pubkey blob truncated at row {i} scope"))? + .to_vec() + } else { + Vec::new() + }; rows.push(DecodedPubkeyRow { key_id, key_type, @@ -222,6 +240,7 @@ pub(crate) fn parse_pubkey_rows(bytes: &[u8]) -> Result, S pubkey_bytes, contract_bounds_id, contract_bounds_document_type, + contract_bounds_scope, }); } @@ -460,6 +479,20 @@ mod tests { ] } + #[test] + fn scoped_payload_has_independent_length_prefixed_framing() { + let mut bytes = vec![0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 2, 0, 3, 0, 1, 2]; + bytes.extend_from_slice(&[0, 4, 0, 9, 0, 8]); + let decoded = parse_pubkey_rows(&bytes).unwrap(); + assert_eq!(decoded[0].contract_bounds_scope, vec![0, 9, 0, 8]); + assert!(decoded[0].contract_bounds_id.is_none()); + let ffi = decoded[0].to_ffi(); + assert_eq!(ffi.contract_bounds_kind, 3); + assert_eq!(ffi.contract_bounds_scope_len, 4); + bytes.pop(); + assert!(parse_pubkey_rows(&bytes).is_err()); + } + #[test] fn round_trips_the_full_six_key_policy() { let dashpay_id = [7u8; 32]; @@ -539,8 +572,8 @@ mod tests { #[test] fn rejects_invalid_bounds_kind() { let mut rows = vec![base_master()]; - rows[0].bounds = Some((3, [1u8; 32], None)); - // encode() writes kind byte from bounds.0 = 3, then a 32-byte id. + rows[0].bounds = Some((4, [1u8; 32], None)); + // encode() writes kind byte from bounds.0 = 4, then a 32-byte id. let err = parse_pubkey_rows(&encode(&rows)).unwrap_err(); assert!(err.contains("contractBoundsKind"), "{err}"); } diff --git a/packages/rs-unified-sdk-jni/src/transactions.rs b/packages/rs-unified-sdk-jni/src/transactions.rs index 04552c1e9ea..d731a99b7a8 100644 --- a/packages/rs-unified-sdk-jni/src/transactions.rs +++ b/packages/rs-unified-sdk-jni/src/transactions.rs @@ -151,13 +151,15 @@ fn read_cstring(env: &mut JNIEnv, s: &JString, field: &str) -> Option { /// u8 purpose (DPP Purpose discriminant, 0 = AUTHENTICATION) /// u8 security_level (DPP SecurityLevel discriminant, 0 = MASTER) /// u8 read_only (0 / 1) -/// u8 contract_bounds_kind (0 none, 1 SingleContract, 2 SingleContractDocumentType) +/// u8 contract_bounds_kind (0 none, 1 SingleContract, 2 SingleContractDocumentType, 3 Scoped) /// u16 pubkey_len /// u8[pubkey_len] pubkey_bytes (compressed pubkey, or 20-byte HASH160) -/// if contract_bounds_kind != 0: +/// if contract_bounds_kind == 1 or contract_bounds_kind == 2: /// u8[32] contract_bounds_id /// if contract_bounds_kind == 2: /// u16 doc_type_len, u8[doc_type_len] doc_type (UTF-8) +/// if contract_bounds_kind == 3: +/// u16 scope_len, u8[scope_len] versioned DPP scope bytes /// ``` /// /// `disablePublicKeyIds` is a JVM `int[]` of key ids to disable (may be diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/DPP/DPPIdentity.swift b/packages/swift-sdk/Sources/SwiftDashSDK/DPP/DPPIdentity.swift index 9271358c6a7..d214e0cf1aa 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/DPP/DPPIdentity.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/DPP/DPPIdentity.swift @@ -1,4 +1,5 @@ import Foundation +import DashSDKFFI // MARK: - Key Type @@ -167,6 +168,7 @@ public struct IdentityPublicKey: Codable, Equatable, Sendable { // MARK: - Contract Bounds public enum ContractBounds: Codable, Equatable, Sendable, CustomStringConvertible { + case scoped(encodedScope: Data) case singleContract(id: Identifier) case singleContractDocumentType(id: Identifier, documentTypeName: String) @@ -174,9 +176,11 @@ public enum ContractBounds: Codable, Equatable, Sendable, CustomStringConvertibl case type case id case documentType + case encodedScope } private enum BoundType: String, Codable { + case scoped case singleContract case singleContractDocumentType } @@ -186,6 +190,8 @@ public enum ContractBounds: Codable, Equatable, Sendable, CustomStringConvertibl let type = try container.decode(BoundType.self, forKey: .type) switch type { + case .scoped: + self = .scoped(encodedScope: try container.decode(Data.self, forKey: .encodedScope)) case .singleContract: let id = try container.decode(Identifier.self, forKey: .id) self = .singleContract(id: id) @@ -200,6 +206,9 @@ public enum ContractBounds: Codable, Equatable, Sendable, CustomStringConvertibl var container = encoder.container(keyedBy: CodingKeys.self) switch self { + case .scoped(let encodedScope): + try container.encode(BoundType.scoped, forKey: .type) + try container.encode(encodedScope, forKey: .encodedScope) case .singleContract(let id): try container.encode(BoundType.singleContract, forKey: .type) try container.encode(id, forKey: .id) @@ -212,6 +221,8 @@ public enum ContractBounds: Codable, Equatable, Sendable, CustomStringConvertibl public var description: String { switch self { + case .scoped: + return "Scoped application authentication" case .singleContract(let id): return "Limited to contract: \(id.toBase58String())" case .singleContractDocumentType(let id, let docType): @@ -219,8 +230,10 @@ public enum ContractBounds: Codable, Equatable, Sendable, CustomStringConvertibl } } - public var contractId: Identifier { + public var contractId: Identifier? { switch self { + case .scoped: + return nil case .singleContract(let id): return id case .singleContractDocumentType(let id, _): @@ -319,3 +332,38 @@ extension DPPIdentity { ) } } + +extension ContractBounds { + /// Decode bounds returned by Platform without implementing DPP's wire encoding in Swift. + public static func fromPlatformJSON(_ value: Any?) throws -> ContractBounds? { + guard let value, !(value is NSNull) else { return nil } + let input = try JSONSerialization.data(withJSONObject: value) + guard let json = String(data: input, encoding: .utf8) else { + throw SDKError.serializationError("Invalid contract bounds JSON") + } + let result = json.withCString { dash_sdk_contract_bounds_parse_json($0) } + if let error = result.error { + let message = error.pointee.message.map { String(cString: $0) } ?? "Invalid contract bounds" + dash_sdk_error_free(error) + throw SDKError.serializationError(message) + } + guard let raw = result.data else { throw SDKError.serializationError("Missing contract bounds") } + defer { dash_sdk_string_free(raw.assumingMemoryBound(to: CChar.self)) } + let output = Data(String(cString: raw.assumingMemoryBound(to: CChar.self)).utf8) + guard let fields = try JSONSerialization.jsonObject(with: output) as? [String: Any], + let kind = fields["kind"] as? Int else { + throw SDKError.serializationError("Invalid normalized contract bounds") + } + if kind == 3, let bytes = fields["scope"] as? [UInt8] { + return .scoped(encodedScope: Data(bytes)) + } + guard let bytes = fields["id"] as? [UInt8], bytes.count == 32 else { + throw SDKError.serializationError("Missing contract identifier") + } + if kind == 1 { return .singleContract(id: Data(bytes)) } + if kind == 2, let name = fields["documentType"] as? String { + return .singleContractDocumentType(id: Data(bytes), documentTypeName: name) + } + throw SDKError.serializationError("Unknown contract bounds kind") + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift index eaa0ff44317..eb31ef6ae5d 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift @@ -123,7 +123,50 @@ public enum DashModelContainer { + [PersistentTrackedMasternode.self] } - /// All persistent model types in the current Dash SDK schema (V4). + /// The exact model set shipped as schema V4. The relationship component + /// is frozen at its pre-scoped-authentication shape so that adding the + /// scope column below cannot mutate V4's checksum. + fileprivate static var v4ModelTypes: [any PersistentModel.Type] { + [ + DashSchemaV4.PersistentIdentity.self, + DashSchemaV4.PersistentDPNSName.self, + DashSchemaV4.PersistentDashpayProfile.self, + DashSchemaV4.PersistentDashpayContactProfile.self, + DashSchemaV4.PersistentDashpayContactRequest.self, + DashSchemaV4.PersistentDashpayPayment.self, + DashSchemaV4.PersistentDashpayIgnoredSender.self, + DashSchemaV4.PersistentDocument.self, + DashSchemaV4.PersistentDataContract.self, + DashSchemaV4.PersistentPublicKey.self, + DashSchemaV4.PersistentTokenBalance.self, + DashSchemaV4.PersistentKeyword.self, + DashSchemaV4.PersistentToken.self, + DashSchemaV4.PersistentDocumentType.self, + DashSchemaV4.PersistentIndex.self, + DashSchemaV4.PersistentProperty.self, + DashSchemaV4.PersistentTokenHistoryEvent.self, + DashSchemaV4.PersistentPlatformAddress.self, + PersistentPlatformAddressesSyncState.self, + DashSchemaV4.PersistentWallet.self, + DashSchemaV4.PersistentAccount.self, + DashSchemaV4.PersistentCoreAddress.self, + DashSchemaV4.PersistentTransaction.self, + DashSchemaV4.PersistentTxo.self, + DashSchemaV4.PersistentPendingInput.self, + PersistentWalletManagerMetadata.self, + PersistentShieldedNote.self, + PersistentShieldedOutgoingNote.self, + PersistentShieldedSyncState.self, + PersistentShieldedActivity.self, + PersistentShieldedViewingKey.self, + PersistentAssetLock.self, + PersistentInvitation.self, + PersistentMasternode.self, + PersistentTrackedMasternode.self, + ] + } + + /// All persistent model types in the current Dash SDK schema (V5). /// 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. @@ -133,7 +176,7 @@ public enum DashModelContainer { /// Create the schema for all Dash Platform models public static var schema: Schema { - Schema(versionedSchema: DashSchemaV4.self) + Schema(versionedSchema: DashSchemaV5.self) } /// Create a persistent model container for storing data @@ -181,14 +224,15 @@ public enum DashModelContainer { /// SwiftData migration plan for Dash Platform model updates public enum DashMigrationPlan: SchemaMigrationPlan { public static var schemas: [any VersionedSchema.Type] { - [DashSchemaV1.self, DashSchemaV2.self, DashSchemaV3.self, DashSchemaV4.self] + [DashSchemaV1.self, DashSchemaV2.self, DashSchemaV3.self, DashSchemaV4.self, DashSchemaV5.self] } public static var stages: [MigrationStage] { [ .lightweight(fromVersion: DashSchemaV1.self, toVersion: DashSchemaV2.self), .lightweight(fromVersion: DashSchemaV2.self, toVersion: DashSchemaV3.self), - .lightweight(fromVersion: DashSchemaV3.self, toVersion: DashSchemaV4.self) + .lightweight(fromVersion: DashSchemaV3.self, toVersion: DashSchemaV4.self), + .lightweight(fromVersion: DashSchemaV4.self, toVersion: DashSchemaV5.self) ] } } @@ -390,6 +434,17 @@ public enum DashSchemaV4: VersionedSchema { Schema.Version(4, 0, 0) } + public static var models: [any PersistentModel.Type] { + DashModelContainer.v4ModelTypes + } +} + +/// Version 5 adds `PersistentPublicKey.contractBoundsScope`. V4 references +/// the frozen relationship component, so this stage is a normal additive +/// migration from stores written by the previous release. +public enum DashSchemaV5: VersionedSchema { + public static var versionIdentifier: Schema.Version { Schema.Version(5, 0, 0) } + public static var models: [any PersistentModel.Type] { DashModelContainer.modelTypes } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaFrozenModels.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaFrozenModels.swift index 1b535ec001d..d192430df35 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaFrozenModels.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaFrozenModels.swift @@ -29,13 +29,14 @@ import SwiftData // // - `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 +// `DashSchemaV1.models` and `DashSchemaV2.models`; V3 and later 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 +// V4 uses its own snapshot in `DashSchemaV4FrozenModels.swift`; V5 +// 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 diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaV4FrozenModels.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaV4FrozenModels.swift new file mode 100644 index 00000000000..eacb8a6c7cc --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashSchemaV4FrozenModels.swift @@ -0,0 +1,1159 @@ +import Foundation +import SwiftData + +// The V4 relationship component, frozen before contractBoundsScope was added. +// SwiftData identifies a store by its entity checksum, so V4 must never use +// the live models again. All 24 related models travel together: relationships +// and inverse key paths must resolve to this same set of nested types. +// Keep stored properties, defaults, indexes and relationships unchanged. +// Initializers are retained for migration fixtures; runtime helpers stay on +// the live models. See DashSchemaFrozenModels.swift for earlier versions. + +extension DashSchemaV4 { + @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 = [] + } + } + + @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() + } + } + + @Model + final class PersistentDPNSName { + #Unique([\.networkRaw, \.normalizedParentDomainName, \.normalizedLabel]) + var networkRaw: UInt32 + 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() + } + + 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()) + } + }) + } + } + + @Model + final class PersistentDashpayContactProfile { + #Unique([ + \.networkRaw, \.ownerIdentityId, \.contactIdentityId + ]) + var networkRaw: UInt32 + 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() + } + } + + @Model + final class PersistentDashpayContactRequest { + #Unique([ + \.networkRaw, \.ownerIdentityId, \.contactIdentityId, \.isOutgoing + ]) + var networkRaw: UInt32 + 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() + } + } + + @Model + final class PersistentDashpayIgnoredSender { + #Unique([ + \.networkRaw, \.ownerIdentityId, \.ignoredSenderId + ]) + var networkRaw: UInt32 + 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() + } + } + + @Model + final class PersistentDashpayPayment { + #Unique([ + \.networkRaw, \.ownerIdentityId, \.txid + ]) + var networkRaw: UInt32 + var ownerIdentityId: Data + var counterpartyIdentityId: Data + var amountDuffs: UInt64 + var directionRaw: UInt8 + var statusRaw: UInt8 + 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() + } + } + + @Model + final class PersistentDashpayProfile { + #Unique([\.networkRaw, \.identity]) + var networkRaw: UInt32 + 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() + } + } + + @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 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? + 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 + } + } + + @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 isDeleted: Bool = false + var localCreatedAt: Date + var localUpdatedAt: Date + var documentType_relation: PersistentDocumentType? + var dataContract: PersistentDataContract? + var ownerIdentity: PersistentIdentity? + 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() + } + } + + @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() + } + } + + @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 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 + } + } + + @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() + } + } + + @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 + } + } + + @Model + final class PersistentPendingInput { + #Index([\.outpoint], [\.walletId], [\.walletId, \.isSweptTombstone]) + var outpoint: Data + var inputIndex: UInt32 + var spendingTxid: Data + var spendingTransaction: PersistentTransaction? + var walletId: Data + var createdAt: Date + var isSweptTombstone: Bool = false + var winnerMinedHeight: UInt32? + 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]) + @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() + } + } + + @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() + } + } + + @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() + } + } + + @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() + } + } + + @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 + @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 + } + } + + @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() + } + } + + @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() + } + } + + @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 supersededByTxid: Data? + 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 + } + } + + @Model + final class PersistentWallet { + #Index([\.networkRaw], [\.walletGroupId]) + #Unique([\.walletId]) + var walletId: Data + var walletGroupId: Data = Data() + var networkRaw: UInt32? + var name: String? + var walletDescription: String? + var birthHeight: UInt32 + var syncedHeight: UInt32 + var lastSynced: UInt64 + var lastAppliedChainLockBytes: Data? + var lastAppliedChainLockHeight: UInt32? + 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 = [] + } + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentPublicKey.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentPublicKey.swift index 5ed049b04fb..cdf339c9d8f 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentPublicKey.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentPublicKey.swift @@ -34,6 +34,9 @@ public final class PersistentPublicKey { /// `.singleContract(id:)`. Optional so old stores load cleanly. public var contractBoundsDocumentTypeName: String? + /// Versioned DPP scope bytes. Optional for lightweight migration of existing stores. + public var contractBoundsScope: Data? + // MARK: - Private Key Reference (optional) public var privateKeyKeychainIdentifier: String? @@ -74,6 +77,7 @@ public final class PersistentPublicKey { disabledAt: Int64? = nil, contractBounds: [Data]? = nil, contractBoundsDocumentTypeName: String? = nil, + contractBoundsScope: Data? = nil, identityId: String ) { self.keyId = keyId @@ -89,6 +93,7 @@ public final class PersistentPublicKey { self.contractBoundsData = nil } self.contractBoundsDocumentTypeName = contractBoundsDocumentTypeName + self.contractBoundsScope = contractBoundsScope self.identityId = identityId self.createdAt = Date() } @@ -116,6 +121,7 @@ public final class PersistentPublicKey { // `PersistentPublicKey.from(IdentityPublicKey, identityId:)` // which sets both columns atomically. contractBoundsDocumentTypeName = nil + contractBoundsScope = nil if let newValue = newValue { contractBoundsData = try? JSONSerialization.data(withJSONObject: newValue.map { $0.base64EncodedString() }) } else { @@ -178,7 +184,9 @@ extension PersistentPublicKey { // rejected here. Drop the bounds projection on length // mismatch — the rest of the key is still recoverable. let bounds: ContractBounds? - if let id = contractBounds?.first, id.count == 32 { + if let encodedScope = contractBoundsScope { + bounds = .scoped(encodedScope: encodedScope) + } else if let id = contractBounds?.first, id.count == 32 { if let docTypeName = contractBoundsDocumentTypeName, !docTypeName.isEmpty { bounds = .singleContractDocumentType(id: id, documentTypeName: docTypeName) } else { @@ -205,7 +213,12 @@ extension PersistentPublicKey { public static func from(_ publicKey: IdentityPublicKey, identityId: String) -> PersistentPublicKey? { let boundsIds: [Data]? let docTypeName: String? + var scope: Data? switch publicKey.contractBounds { + case .scoped(let encodedScope): + boundsIds = nil + docTypeName = nil + scope = encodedScope case .singleContract(let id): boundsIds = [id] docTypeName = nil @@ -226,6 +239,7 @@ extension PersistentPublicKey { disabledAt: publicKey.disabledAt.map { Int64($0) }, contractBounds: boundsIds, contractBoundsDocumentTypeName: docTypeName, + contractBoundsScope: scope, identityId: identityId ) } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedIdentity.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedIdentity.swift index 182e49b380c..cfa3a69b766 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedIdentity.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedIdentity.swift @@ -84,8 +84,7 @@ public final class ManagedIdentity: @unchecked Sendable { } /// Snapshot of an identity's registered public keys. Mirrors the - /// DPP `IdentityPublicKeyV0` shape. Contract bounds aren't - /// included yet — see the FFI docstring. + /// DPP `IdentityPublicKeyV0` shape, including the complete contract bounds. public struct IdentityPublicKeyInfo: Sendable { public let keyId: Int32 public let purpose: KeyPurpose @@ -99,6 +98,7 @@ public final class ManagedIdentity: @unchecked Sendable { /// (compressed secp256k1 pubkey for ECDSA, hash160 for /// HASH160 variants, etc.). public let data: Data + public let contractBounds: ContractBounds? } /// Return every `IdentityPublicKey` registered on this identity. @@ -143,6 +143,13 @@ public final class ManagedIdentity: @unchecked Sendable { data = Data() } + let bounds: ContractBounds? + if let json = ffi.contract_bounds_json { + let value = try JSONSerialization.jsonObject(with: Data(String(cString: json).utf8)) + bounds = try ContractBounds.fromPlatformJSON(value) + } else { + bounds = nil + } keys.append( IdentityPublicKeyInfo( keyId: Int32(bitPattern: ffi.key_id), @@ -153,7 +160,8 @@ public final class ManagedIdentity: @unchecked Sendable { disabledAt: ffi.disabled_at_is_some ? Int64(bitPattern: ffi.disabled_at) : nil, - data: data + data: data, + contractBounds: bounds ) ) } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift index 8664e1dfb90..74e180e2862 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift @@ -200,9 +200,10 @@ public final class ManagedPlatformWallet: @unchecked Sendable { } /// Swift mirror of `dpp::identity::identity_public_key::contract_bounds::ContractBounds`. - /// Pinned to two variants (no `MultipleContractsOfSameOwner`) - /// to match the Rust enum's currently-supported shape. + /// Scoped authentication grants retain their versioned DPP encoding. public enum ContractBounds: Sendable, Equatable { + /// Versioned scope bytes produced by DPP; validated by Rust on registration. + case scoped(encodedScope: Data) /// Key may be used within a specific contract (any /// document type). Maps to `kind == 1` on the FFI side. case singleContract(id: Data) @@ -669,7 +670,7 @@ public final class ManagedPlatformWallet: @unchecked Sendable { let pk = pubkeys[index] return buffers[index].withUnsafeBytes { (raw: UnsafeRawBufferPointer) -> R in let basePtr = raw.bindMemory(to: UInt8.self).baseAddress - return pinContractBounds(pk.contractBounds) { kind, idPtr, docTypePtr in + return pinContractBounds(pk.contractBounds) { kind, idPtr, docTypePtr, scopePtr, scopeLen in rows.append( IdentityPubkeyFFI( key_id: pk.keyId, @@ -681,7 +682,9 @@ public final class ManagedPlatformWallet: @unchecked Sendable { read_only: pk.readOnly, contract_bounds_kind: kind, contract_bounds_id: idPtr, - contract_bounds_document_type: docTypePtr + contract_bounds_document_type: docTypePtr, + contract_bounds_scope: scopePtr, + contract_bounds_scope_len: scopeLen ) ) return pinNext(index + 1, &rows, pubkeys, buffers, body) @@ -696,11 +699,15 @@ public final class ManagedPlatformWallet: @unchecked Sendable { /// inside `pinNext`. private static func pinContractBounds( _ bounds: ContractBounds?, - _ body: (UInt8, UnsafePointer?, UnsafePointer?) -> R + _ body: (UInt8, UnsafePointer?, UnsafePointer?, UnsafePointer?, UInt) -> R ) -> R { switch bounds { case .none: - return body(0, nil, nil) + return body(0, nil, nil, nil, 0) + case .scoped(let encodedScope): + return encodedScope.withUnsafeBytes { raw in + body(3, nil, nil, raw.bindMemory(to: UInt8.self).baseAddress, UInt(raw.count)) + } case .singleContract(let id): // The Rust side reads exactly 32 bytes off // `contract_bounds_id`. A short or empty `Data` would @@ -714,7 +721,7 @@ public final class ManagedPlatformWallet: @unchecked Sendable { ) return id.withUnsafeBytes { raw -> R in let idPtr = raw.bindMemory(to: UInt8.self).baseAddress - return body(1, idPtr, nil) + return body(1, idPtr, nil, nil, 0) } case .singleContractDocumentType(let id, let documentTypeName): precondition( @@ -724,7 +731,7 @@ public final class ManagedPlatformWallet: @unchecked Sendable { return id.withUnsafeBytes { raw -> R in let idPtr = raw.bindMemory(to: UInt8.self).baseAddress return documentTypeName.withCString { docTypePtr in - body(2, idPtr, docTypePtr) + body(2, idPtr, docTypePtr, nil, 0) } } } @@ -3453,6 +3460,11 @@ extension ManagedPlatformWallet { id: Swift.withUnsafeBytes(of: &idTuple) { Data($0) }, documentTypeName: documentTypeName ) + case 3: + guard let scope = entry.contract_bounds_scope, entry.contract_bounds_scope_len > 0 else { + throw PlatformWalletError.deserialization("Missing authentication scope at key \(index)") + } + return .scoped(encodedScope: Data(bytes: scope, count: Int(entry.contract_bounds_scope_len))) default: throw PlatformWalletError.deserialization( "Unknown IdentityUpdateTransition contract-bounds kind \(entry.contract_bounds_kind) at key \(index)" diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift index 74b7446411e..43e8ded7a21 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift @@ -3821,7 +3821,12 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { // and reconstruct as `.singleContract`. let snapshotBoundsIds: [Data]? let snapshotBoundsDocType: String? + var snapshotScope: Data? switch entry.contractBounds { + case .some(.scoped(let encodedScope)): + snapshotBoundsIds = nil + snapshotBoundsDocType = nil + snapshotScope = encodedScope case .some(.singleContract(let id)): snapshotBoundsIds = [id] snapshotBoundsDocType = nil @@ -3877,6 +3882,7 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { // scope) must overwrite any stale value here. row.contractBounds = snapshotBoundsIds row.contractBoundsDocumentTypeName = snapshotBoundsDocType + row.contractBoundsScope = snapshotScope // Private-key handling: no secret crosses the FFI. A // wallet-derivable key whose private bytes were materialized by @@ -7852,7 +7858,16 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { // wrong-length id falls back to "no bounds" // rather than crashing FFI marshalling on the // Rust side. - if let id = pk.contractBounds?.first, id.count == 32 { + if let scope = pk.contractBoundsScope { + row.contract_bounds_kind = 3 + if !scope.isEmpty { + let scopeBuf = UnsafeMutablePointer.allocate(capacity: scope.count) + scope.copyBytes(to: scopeBuf, count: scope.count) + allocation.scalarBuffers.append((scopeBuf, scope.count)) + row.contract_bounds_scope = UnsafePointer(scopeBuf) + row.contract_bounds_scope_len = UInt(scope.count) + } + } else if let id = pk.contractBounds?.first, id.count == 32 { withUnsafeMutableBytes(of: &row.contract_bounds_id) { dst in id.copyBytes(to: dst.bindMemory(to: UInt8.self).baseAddress!, count: 32) } @@ -9519,8 +9534,15 @@ private func persistIdentityKeysCallback( } else { bounds = nil } - default: + case 3: + guard let scope = e.contract_bounds_scope, e.contract_bounds_scope_len > 0 else { + return -1 + } + bounds = .scoped(encodedScope: Data(bytes: scope, count: Int(e.contract_bounds_scope_len))) + case 0: bounds = nil + default: + return -1 } upserts.append(.init( diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/IdentityKeyRefresher.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/IdentityKeyRefresher.swift index 7282187529d..2ba42dc4e94 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/IdentityKeyRefresher.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/IdentityKeyRefresher.swift @@ -59,32 +59,37 @@ enum IdentityKeyRefresher { } // Public keys — parse the freshly-fetched set. - var parsedPublicKeys: [IdentityPublicKey] = [] - if let publicKeysArray = fetchedIdentity["publicKeys"] as? [[String: Any]] { - parsedPublicKeys = publicKeysArray.compactMap { keyData -> IdentityPublicKey? in - guard let id = keyData["id"] as? Int, - let purpose = keyData["purpose"] as? Int, - let securityLevel = keyData["securityLevel"] as? Int, - let keyType = keyData["type"] as? Int, - let dataStr = keyData["data"] as? String, - let data = Data(base64Encoded: dataStr) else { - return nil - } + let publicKeysArray: [[String: Any]] + if let rows = fetchedIdentity["publicKeys"] as? [[String: Any]] { + publicKeysArray = rows + } else if let rows = fetchedIdentity["publicKeys"] as? [String: [String: Any]] { + publicKeysArray = Array(rows.values) + } else { + throw SDKError.serializationError("Identity response is missing public keys") + } + let parsedPublicKeys = try publicKeysArray.compactMap { keyData -> IdentityPublicKey? in + guard let id = keyData["id"] as? Int, + let purpose = keyData["purpose"] as? Int, + let securityLevel = keyData["securityLevel"] as? Int, + let keyType = keyData["type"] as? Int, + let dataStr = keyData["data"] as? String, + let data = Data(base64Encoded: dataStr) else { + return nil + } - let readOnly = keyData["readOnly"] as? Bool ?? false - let disabledAt = keyData["disabledAt"] as? UInt64 + let readOnly = keyData["readOnly"] as? Bool ?? false + let disabledAt = keyData["disabledAt"] as? UInt64 - return IdentityPublicKey( - id: UInt32(id), - purpose: KeyPurpose(rawValue: UInt8(purpose)) ?? .authentication, - securityLevel: SecurityLevel(rawValue: UInt8(securityLevel)) ?? .high, - contractBounds: nil, - keyType: KeyType(rawValue: UInt8(keyType)) ?? .ecdsaSecp256k1, - readOnly: readOnly, - data: data, - disabledAt: disabledAt - ) - } + return IdentityPublicKey( + id: UInt32(id), + purpose: KeyPurpose(rawValue: UInt8(purpose)) ?? .authentication, + securityLevel: SecurityLevel(rawValue: UInt8(securityLevel)) ?? .high, + contractBounds: try ContractBounds.fromPlatformJSON(keyData["contractBounds"]), + keyType: KeyType(rawValue: UInt8(keyType)) ?? .ecdsaSecp256k1, + readOnly: readOnly, + data: data, + disabledAt: disabledAt + ) } // Replace the PersistentIdentity's public key rows with the diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/LoadIdentityView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/LoadIdentityView.swift index 432cd0cd4d7..893f455eaac 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/LoadIdentityView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/LoadIdentityView.swift @@ -332,7 +332,7 @@ struct LoadIdentityView: View { // The publicKeys might be a dictionary with key IDs as keys if let publicKeysDict = identityData["publicKeys"] as? [String: Any] { print("🔵 Public keys are in dictionary format") - parsedPublicKeys = publicKeysDict.compactMap { (keyIdStr, keyData) -> IdentityPublicKey? in + parsedPublicKeys = try publicKeysDict.compactMap { (keyIdStr, keyData) -> IdentityPublicKey? in guard let keyData = keyData as? [String: Any], let id = Int(keyIdStr) ?? keyData["id"] as? Int, let purpose = keyData["purpose"] as? Int, @@ -356,7 +356,7 @@ struct LoadIdentityView: View { id: UInt32(id), purpose: KeyPurpose(rawValue: UInt8(purpose)) ?? .authentication, securityLevel: SecurityLevel(rawValue: UInt8(securityLevel)) ?? .high, - contractBounds: nil, + contractBounds: try ContractBounds.fromPlatformJSON(keyData["contractBounds"]), keyType: KeyType(rawValue: UInt8(keyType)) ?? .ecdsaSecp256k1, readOnly: readOnly, data: data, @@ -365,7 +365,7 @@ struct LoadIdentityView: View { } } else if let publicKeysArray = identityData["publicKeys"] as? [[String: Any]] { print("🔵 Public keys are in array format") - parsedPublicKeys = publicKeysArray.compactMap { keyData -> IdentityPublicKey? in + parsedPublicKeys = try publicKeysArray.compactMap { keyData -> IdentityPublicKey? in guard let id = keyData["id"] as? Int, let purpose = keyData["purpose"] as? Int, let securityLevel = keyData["securityLevel"] as? Int, @@ -388,7 +388,7 @@ struct LoadIdentityView: View { id: UInt32(id), purpose: KeyPurpose(rawValue: UInt8(purpose)) ?? .authentication, securityLevel: SecurityLevel(rawValue: UInt8(securityLevel)) ?? .high, - contractBounds: nil, + contractBounds: try ContractBounds.fromPlatformJSON(keyData["contractBounds"]), keyType: KeyType(rawValue: UInt8(keyType)) ?? .ecdsaSecp256k1, readOnly: readOnly, data: data, diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageRecordDetailViews.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageRecordDetailViews.swift index d5394d9436a..ddf5e4c3291 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageRecordDetailViews.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageRecordDetailViews.swift @@ -765,7 +765,9 @@ struct PublicKeyStorageDetailView: View { } Section("Data") { FieldRow(label: "Public Key", value: hexString(record.publicKeyData)) - if let bounds = record.contractBounds, !bounds.isEmpty { + if record.contractBoundsScope != nil { + FieldRow(label: "Contract Bounds", value: "Scoped authentication") + } else if let bounds = record.contractBounds, !bounds.isEmpty { FieldRow(label: "Contract Bounds", value: "\(bounds.count)") ForEach(Array(bounds.enumerated()), id: \.offset) { _, contractId in FieldRow(label: "Contract", value: contractId.toBase58String()) diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift index 08d1f84c383..6cb34c51344 100644 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift @@ -5,6 +5,77 @@ import XCTest @testable import SwiftDashSDK final class DashModelMigrationTests: XCTestCase { + @MainActor + func testV4PublicKeysMigrateToV5AndPersistAuthenticationScope() 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 storeURL = directory.appendingPathComponent("dash.store") + let walletId = Data(repeating: 7, count: 32) + let identityId = Data(repeating: 8, count: 32) + let keyData = Data(repeating: 2, count: 33) + let keychainIdentifier = "migration-key-reference" + + // Use the historical shape: a store written with the new live model + // would hide the unknown-schema regression this test guards against. + do { + let schema = Schema(versionedSchema: DashSchemaV4.self) + let keyEntity = try XCTUnwrap(schema.entities.first { $0.name == "PersistentPublicKey" }) + XCTAssertNil(keyEntity.attributesByName["contractBoundsScope"]) + let configuration = ModelConfiguration( + schema: schema, url: storeURL, cloudKitDatabase: .none) + let container = try ModelContainer(for: schema, configurations: [configuration]) + let wallet = DashSchemaV4.PersistentWallet(walletId: walletId, network: .testnet) + let identity = DashSchemaV4.PersistentIdentity(identityId: identityId, network: .testnet) + let key = DashSchemaV4.PersistentPublicKey( + keyId: 3, + purpose: .authentication, + securityLevel: .high, + keyType: .ecdsaSecp256k1, + publicKeyData: keyData, + identityId: identityId.toBase58String()) + container.mainContext.insert(wallet) + container.mainContext.insert(identity) + container.mainContext.insert(key) + identity.wallet = wallet + key.identity = identity + key.privateKeyKeychainIdentifier = keychainIdentifier + try container.mainContext.save() + } + + let schema = DashModelContainer.schema + let configuration = ModelConfiguration( + schema: schema, url: storeURL, cloudKitDatabase: .none) + let scope = Data([0, 1, 2, 3, 65, 0]) + do { + let container = try ModelContainer( + for: schema, migrationPlan: DashMigrationPlan.self, configurations: [configuration]) + let keys = try container.mainContext.fetch(FetchDescriptor()) + XCTAssertEqual(keys.count, 1) + let key = try XCTUnwrap(keys.first) + XCTAssertEqual(key.keyId, 3) + XCTAssertEqual(key.publicKeyData, keyData) + XCTAssertEqual(key.privateKeyKeychainIdentifier, keychainIdentifier) + XCTAssertEqual(key.identity?.identityId, identityId) + XCTAssertEqual(key.identity?.wallet?.walletId, walletId) + XCTAssertEqual(key.identity?.publicKeys.count, 1) + XCTAssertNil(key.contractBoundsScope) + key.contractBoundsScope = scope + try container.mainContext.save() + } + + // Reopening proves the new bytes are durable, not just retained by + // the context's cached instance of the migrated key. + let reopened = try ModelContainer( + for: schema, migrationPlan: DashMigrationPlan.self, configurations: [configuration]) + let key = try XCTUnwrap( + reopened.mainContext.fetch(FetchDescriptor()).first) + XCTAssertEqual(key.contractBoundsScope, scope) + XCTAssertEqual(key.privateKeyKeychainIdentifier, keychainIdentifier) + XCTAssertEqual(key.identity?.wallet?.walletId, walletId) + } + @MainActor func testV1StoreMigratesToV2AndAcceptsTrackedMasternodes() throws { let directory = FileManager.default.temporaryDirectory @@ -67,7 +138,7 @@ final class DashModelMigrationTests: XCTestCase { /// 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 - /// frozen type and comes out as the live one — which is the whole point + /// V3 frozen type and comes out as the V4 frozen 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 /// migration too. @@ -134,18 +205,18 @@ final class DashModelMigrationTests: XCTestCase { configurations: [v4Configuration]) let wallets = try migrated.mainContext.fetch( - FetchDescriptor()) + FetchDescriptor()) XCTAssertEqual(wallets.count, 1, "the V3 row must survive the migration") XCTAssertNil( wallets.first?.lastAppliedChainLockHeight, "a wallet migrated from V3 has no chainlock boundary yet, so no " + "tombstone it later takes can be collected on a fabricated one") let pending = try migrated.mainContext.fetch( - FetchDescriptor()) + FetchDescriptor()) XCTAssertEqual(pending.count, 1, "the V3 pending row must survive the migration") XCTAssertEqual(pending.first?.isSweptTombstone, false, "backfilled as an ordinary claim") XCTAssertNil(pending.first?.winnerMinedHeight, "and unstamped") - let coins = try migrated.mainContext.fetch(FetchDescriptor()) + let coins = try migrated.mainContext.fetch(FetchDescriptor()) XCTAssertEqual(coins.count, 1, "the V3 TXO row must survive the migration") XCTAssertEqual(coins.first?.isSpent, true, "its spent flag is carried as stored") XCTAssertNil( @@ -153,7 +224,7 @@ final class DashModelMigrationTests: XCTestCase { "a coin migrated from V3 was never held by a sweep — the stamp backfills to nil, " + "so the release and re-delivery rules see an ordinary spent coin") let transactions = try migrated.mainContext.fetch( - FetchDescriptor()) + FetchDescriptor()) XCTAssertEqual(transactions.map(\.context), [2], "the V3 transaction row survives unchanged") } @@ -161,7 +232,7 @@ final class DashModelMigrationTests: XCTestCase { /// 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, - /// so the rows go in as frozen types and come out live — the property + /// so the rows move between the matching frozen types — the property /// the freeze exists to guarantee, pinned here where it matters most. @MainActor func testV1StoreWithWalletTransactionAndCoinMigratesToV4() throws { @@ -218,15 +289,15 @@ final class DashModelMigrationTests: XCTestCase { migrationPlan: DashMigrationPlan.self, configurations: [v4Configuration]) - let wallets = try migrated.mainContext.fetch(FetchDescriptor()) + let wallets = try migrated.mainContext.fetch(FetchDescriptor()) XCTAssertEqual(wallets.map(\.walletId), [walletId]) XCTAssertNil(wallets.first?.lastAppliedChainLockHeight) let transactions = try migrated.mainContext.fetch( - FetchDescriptor()) + FetchDescriptor()) XCTAssertEqual(transactions.map(\.txid), [txid]) XCTAssertEqual(transactions.first?.context, 3) XCTAssertEqual(transactions.first?.netAmount, 2_000) - let coins = try migrated.mainContext.fetch(FetchDescriptor()) + let coins = try migrated.mainContext.fetch(FetchDescriptor()) XCTAssertEqual(coins.count, 1) XCTAssertEqual(coins.first?.vout, 1) XCTAssertEqual(coins.first?.amount, 2_000) diff --git a/packages/wasm-dpp2/src/data_contract/contract_bounds.rs b/packages/wasm-dpp2/src/data_contract/contract_bounds.rs index ac720b34fed..cb695cb4798 100644 --- a/packages/wasm-dpp2/src/data_contract/contract_bounds.rs +++ b/packages/wasm-dpp2/src/data_contract/contract_bounds.rs @@ -4,29 +4,95 @@ use crate::identifier::{IdentifierLikeJs, IdentifierWasm}; use crate::impl_try_from_js_value; use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; -use dpp::identity::contract_bounds::ContractBounds; +use dpp::identity::contract_bounds::{ + AuthenticationScope, ContractBounds, authentication_scope::permissions, +}; use dpp::prelude::Identifier; +use dpp::serialization::JsonConvertible; +use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; +/// Combine explicitly granted actions with bitwise OR. +#[wasm_bindgen] +#[derive(Clone, Copy, Debug)] +pub enum AuthenticationPermission { + DocumentCreate = 1, + DocumentReplace = 2, + DocumentDelete = 4, + DocumentTransfer = 8, + DocumentUpdatePrice = 16, + DocumentPurchase = 32, + DocumentTokenPayment = 64, + TokenBurn = 128, + TokenMint = 256, + TokenTransfer = 512, + TokenFreeze = 1024, + TokenUnfreeze = 2048, + TokenDestroyFrozenFunds = 4096, + TokenClaim = 8192, + TokenEmergencyAction = 16384, + TokenConfigUpdate = 32768, + TokenDirectPurchase = 65536, + TokenSetPrice = 131072, +} +// wasm-bindgen requires literal discriminants; keep them tied to consensus bits. +const _: () = { + assert!(AuthenticationPermission::DocumentCreate as u32 == permissions::DOCUMENT_CREATE); + assert!(AuthenticationPermission::DocumentReplace as u32 == permissions::DOCUMENT_REPLACE); + assert!(AuthenticationPermission::DocumentDelete as u32 == permissions::DOCUMENT_DELETE); + assert!(AuthenticationPermission::DocumentTransfer as u32 == permissions::DOCUMENT_TRANSFER); + assert!( + AuthenticationPermission::DocumentUpdatePrice as u32 == permissions::DOCUMENT_UPDATE_PRICE + ); + assert!(AuthenticationPermission::DocumentPurchase as u32 == permissions::DOCUMENT_PURCHASE); + assert!( + AuthenticationPermission::DocumentTokenPayment as u32 + == permissions::DOCUMENT_TOKEN_PAYMENT + ); + assert!(AuthenticationPermission::TokenBurn as u32 == permissions::TOKEN_BURN); + assert!(AuthenticationPermission::TokenMint as u32 == permissions::TOKEN_MINT); + assert!(AuthenticationPermission::TokenTransfer as u32 == permissions::TOKEN_TRANSFER); + assert!(AuthenticationPermission::TokenFreeze as u32 == permissions::TOKEN_FREEZE); + assert!(AuthenticationPermission::TokenUnfreeze as u32 == permissions::TOKEN_UNFREEZE); + assert!( + AuthenticationPermission::TokenDestroyFrozenFunds as u32 + == permissions::TOKEN_DESTROY_FROZEN_FUNDS + ); + assert!(AuthenticationPermission::TokenClaim as u32 == permissions::TOKEN_CLAIM); + assert!( + AuthenticationPermission::TokenEmergencyAction as u32 + == permissions::TOKEN_EMERGENCY_ACTION + ); + assert!(AuthenticationPermission::TokenConfigUpdate as u32 == permissions::TOKEN_CONFIG_UPDATE); + assert!( + AuthenticationPermission::TokenDirectPurchase as u32 == permissions::TOKEN_DIRECT_PURCHASE + ); + assert!(AuthenticationPermission::TokenSetPrice as u32 == permissions::TOKEN_SET_PRICE); +}; + #[wasm_bindgen(typescript_custom_section)] const TS_TYPES: &str = r#" -/** - * ContractBounds serialized as a plain object. - */ -export interface ContractBoundsObject { - identifier: Uint8Array; - documentTypeName?: string; - contractBoundsType: "SingleContract" | "SingleContractDocumentType"; +export interface ContractScopeInput { id: string; documentTypes?: string[] | null; } +export interface AuthenticationScopeJSON { + $formatVersion: "0"; + contracts: ContractScopeInput[]; + permissions: number; + expiresAt: number | string | null; } /** - * ContractBounds serialized as JSON. + * ContractBounds serialized as a plain object. */ -export interface ContractBoundsJSON { - identifier: string; - documentTypeName?: string; - contractBoundsType: "SingleContract" | "SingleContractDocumentType"; -} +export type ContractBoundsObject = + | { $type: "singleContract"; id: Uint8Array } + | { $type: "documentType"; id: Uint8Array; documentTypeName: string } + | { $type: "scoped"; $formatVersion: "0"; contracts: { id: Uint8Array; documentTypes?: string[] | null }[]; permissions: number; expiresAt?: bigint | null }; + +/** ContractBounds serialized as JSON. */ +export type ContractBoundsJSON = + | { $type: "singleContract"; id: string } + | { $type: "documentType"; id: string; documentTypeName: string } + | ({ $type: "scoped" } & AuthenticationScopeJSON); "#; #[wasm_bindgen] @@ -99,6 +165,41 @@ impl ContractBoundsWasm { )) } + /// Creates an application delegation. Sort order is canonicalized by the + /// constructor; duplicates/empty restrictions remain errors. + #[wasm_bindgen(js_name = "Scoped")] + pub fn scoped( + contracts: JsValue, + permissions: u32, + expires_at: Option, + ) -> WasmDppResult { + let contracts_json: serde_json::Value = serde_wasm_bindgen::from_value(contracts) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + let mut scope = AuthenticationScope::from_json(serde_json::json!({ + "$formatVersion": "0", "contracts": contracts_json, + "permissions": permissions, "expiresAt": expires_at, + }))?; + let AuthenticationScope::V0(ref mut inner) = scope; + inner.contracts.sort_by_key(|entry| entry.id); + for entry in &mut inner.contracts { + if let Some(names) = &mut entry.document_types { + names.sort(); + } + } + scope.validate()?; + Ok(ContractBoundsWasm(ContractBounds::Scoped(scope))) + } + + #[wasm_bindgen(getter)] + pub fn scope(&self) -> WasmDppResult { + match &self.0 { + ContractBounds::Scoped(scope) => { + crate::serialization::conversions::json_to_js_value(&scope.to_json()?) + } + _ => Ok(JsValue::UNDEFINED), + } + } + #[wasm_bindgen(getter = "identifier")] pub fn id(&self) -> Option { self.0.identifier().copied().map(Into::into) diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index fa7a7772367..a4d0d336197 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -40,6 +40,7 @@ pub use core::pro_tx_hash::{ pub use identity::signer::IdentitySignerWasm; pub use identity::transitions::pooling::PoolingWasm; +pub use data_contract::contract_bounds::AuthenticationPermission; pub use data_contract::{ ContractBoundsWasm, DataContractCreateTransitionWasm, DataContractUpdateTransitionWasm, DataContractWasm, DocumentPropertyReferenceArrayJs, DocumentPropertyReferenceMapJs, diff --git a/packages/wasm-sdk/tests/smoke/scoped-authentication.cjs b/packages/wasm-sdk/tests/smoke/scoped-authentication.cjs new file mode 100644 index 00000000000..42a7938cca6 --- /dev/null +++ b/packages/wasm-sdk/tests/smoke/scoped-authentication.cjs @@ -0,0 +1,56 @@ +/* Run against a real wasm-bindgen --target nodejs build: + * node tests/smoke/scoped-authentication.cjs /absolute/path/to/wasm_sdk.js + */ +const assert = require('node:assert/strict'); +const path = require('node:path'); + +if (!process.argv[2]) throw new Error('Pass the generated wasm_sdk.js module path'); +const wasm = require(path.resolve(process.argv[2])); +const id = '11111111111111111111111111111111'; +const P = wasm.AuthenticationPermission; +const bounds = wasm.ContractBounds.Scoped( + [{ id, documentTypes: ['post', 'like'] }], + P.DocumentCreate | P.DocumentTokenPayment, + 100n, +); +assert.deepEqual(bounds.scope.contracts[0].documentTypes, ['like', 'post']); +assert.deepEqual(wasm.ContractBounds.fromJSON(bounds.toJSON()).toJSON(), bounds.toJSON()); +assert.deepEqual(wasm.ContractBounds.fromObject(bounds.toObject()).toJSON(), bounds.toJSON()); +assert.equal(bounds.identifier, undefined); +assert.throws(() => { bounds.documentTypeName = 'profile'; }); + +for (const [contracts, mask] of [ + [[{ id, documentTypes: [] }], 65], + [[{ id }, { id }], 65], + [[{ id }], 0], + [[{ id }], 1 << 30], + [[{ id, documentTypes: ['a'.repeat(2048)] }], 65], +]) { + assert.throws(() => wasm.ContractBounds.Scoped(contracts, mask)); +} + +const unrestricted = wasm.ContractBounds.Scoped([{ id }], P.DocumentCreate); +const unrestrictedObject = unrestricted.toObject(); +assert.equal(unrestrictedObject.contracts[0].documentTypes, undefined); +assert.equal(unrestrictedObject.expiresAt, undefined); +assert.deepEqual( + wasm.ContractBounds.fromObject(unrestrictedObject).toJSON(), + unrestricted.toJSON(), +); + +const key = new wasm.IdentityPublicKeyInCreation({ + keyId: 2, + purpose: 'authentication', + securityLevel: 'high', + keyType: 'ecdsa_hash160', + isReadOnly: false, + data: new Uint8Array(20).fill(1), + signature: new Uint8Array(), + contractBounds: bounds, +}); +assert.deepEqual(key.contractBounds.toJSON(), bounds.toJSON()); +assert.deepEqual( + wasm.IdentityPublicKeyInCreation.fromJSON(key.toJSON()).contractBounds.toJSON(), + bounds.toJSON(), +); +console.log('Scoped authentication WASM smoke checks passed');