diff --git a/packages/swift-sdk/Package.swift b/packages/swift-sdk/Package.swift index 253d84fcccd..cd9062b83be 100644 --- a/packages/swift-sdk/Package.swift +++ b/packages/swift-sdk/Package.swift @@ -24,7 +24,7 @@ let package = Package( name: "SwiftDashSDK", dependencies: ["DashSDKFFI"], path: "Sources/SwiftDashSDK", - exclude: ["KeyWallet/README.md", "PlatformWallet/README.md"], + exclude: ["KeyWallet/README.md"], linkerSettings: [.linkedFramework("SystemConfiguration")] ), diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Address/Addresses.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Address/Addresses.swift index f5da7a21ca4..1a665d1abf7 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Address/Addresses.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Address/Addresses.swift @@ -1210,45 +1210,6 @@ public class Addresses: @unchecked Sendable { return PlatformAddressInfosResult(infos: infos) } - // MARK: - Convenience Methods - - /// Get the balance for a single address - /// - /// - Parameter addressBytes: Address bytes (21 bytes) - /// - Returns: Balance in credits, or nil if address not found - /// - Throws: SDKError if the query fails - public func getBalance(addressBytes: Data) throws -> UInt64? { - return try getInfo(addressBytes: addressBytes)?.balance - } - - /// Get the nonce for a single address - /// - /// - Parameter addressBytes: Address bytes (21 bytes) - /// - Returns: Nonce value, or nil if address not found - /// - Throws: SDKError if the query fails - public func getNonce(addressBytes: Data) throws -> UInt32? { - return try getInfo(addressBytes: addressBytes)?.nonce - } - - /// Check if an address exists on Platform - /// - /// - Parameter addressBytes: Address bytes (21 bytes) - /// - Returns: true if the address has been used on Platform - /// - Throws: SDKError if the query fails - public func exists(addressBytes: Data) throws -> Bool { - return try getInfo(addressBytes: addressBytes) != nil - } - - /// Get total balance across multiple addresses - /// - /// - Parameter addressesBytesList: Array of address bytes - /// - Returns: Total balance in credits across all found addresses - /// - Throws: SDKError if the query fails - public func getTotalBalance(addressesBytesList: [Data]) throws -> UInt64 { - let result = try getInfos(addressesBytesList: addressesBytesList) - return result.totalBalance - } - // MARK: - Identity State Transitions (Address-Related) /// Top up an identity using Platform address balances diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/ConcurrencyCompat.swift b/packages/swift-sdk/Sources/SwiftDashSDK/ConcurrencyCompat.swift deleted file mode 100644 index bd01f1d0db0..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/ConcurrencyCompat.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation - -// Swift 6 sendability adjustments for FFI pointers and wrappers. -// These are safe under our usage patterns where FFI pointers are thread-confined -// or explicitly synchronized at the Rust boundary. -// -// Swift 6.2+'s standard library ships a conformance -// `@available(*, unavailable) extension OpaquePointer : Sendable {}`, -// which: -// (a) causes app code that previously relied on `OpaquePointer` being -// Sendable to fail with "conformance is unavailable" if we drop -// the retroactive conformance here, and -// (b) trips a "conformance was already stated in the type's module -// 'Swift'" warning — promoted to error by `-warnings-as-errors` — -// if we declare a plain retroactive `@unchecked Sendable`. -// -// Gate on the compiler version: Swift 6.2+ emits the stdlib conformance -// itself (even as unavailable), so we provide `SendableOpaquePointer` -// below as a Sendable wrapper at call sites there; older toolchains -// still benefit from the retroactive shim so existing call sites -// compile unchanged. -#if compiler(<6.2) -extension OpaquePointer: @retroactive @unchecked Sendable {} -#endif - -/// Sendable wrapper around a raw `OpaquePointer`. -/// -/// Swift 6.2's stdlib marks `OpaquePointer: Sendable` as -/// `@available(*, unavailable)`, so crossing a `Task` / `MainActor` -/// boundary with a bare `OpaquePointer` fails strict-concurrency -/// checking. Wrap the pointer in this struct inside the producer -/// closure, extract `.pointer` on the consumer side. Safety is the -/// caller's responsibility — the wrapped pointer is not retained or -/// ref-counted. -public struct SendableOpaquePointer: @unchecked Sendable { - public let pointer: OpaquePointer - - public init(_ pointer: OpaquePointer) { - self.pointer = pointer - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift index e468cc44406..cf7a19ab6be 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift @@ -1,5 +1,4 @@ import Foundation -import LocalAuthentication import Security // MARK: - Wallet Storage @@ -26,9 +25,6 @@ import Security /// with the original name/description after a reinstall. /// * Enumeration of stored wallet ids (used by the orphan-mnemonic /// recovery flow in `ContentView`). -/// * Biometric-protected seed stash at `wallet.biometric` — not yet -/// wired to a caller but kept because it's a different category -/// (hardware-protected rather than a legacy PIN construct). public class WalletStorage { /// Unified keychain service name for the app. Everything the /// SDK writes — per-wallet mnemonics (here), identity private @@ -55,7 +51,6 @@ public class WalletStorage { /// user-facing wallet name and description from the keychain /// even though SwiftData was wiped. public static let metadataAccountPrefix = "wallet.metadata" - private let biometricKeychainAccount = "wallet.biometric" public init() {} @@ -371,66 +366,6 @@ public class WalletStorage { return data } - // MARK: - Biometric Protection - - public func enableBiometricProtection(for seed: Data) throws { - var error: Unmanaged? - guard let access = SecAccessControlCreateWithFlags( - nil, - kSecAttrAccessibleWhenUnlockedThisDeviceOnly, - .biometryCurrentSet, - &error - ) else { - throw WalletStorageError.biometricSetupFailed - } - - let deleteQuery: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: keychainService, - kSecAttrAccount as String: biometricKeychainAccount - ] - - let deleteStatus = SecItemDelete(deleteQuery as CFDictionary) - guard deleteStatus == errSecSuccess || deleteStatus == errSecItemNotFound else { - throw WalletStorageError.keychainError(deleteStatus) - } - - let addQuery: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: keychainService, - kSecAttrAccount as String: biometricKeychainAccount, - kSecValueData as String: seed, - kSecAttrAccessControl as String: access - ] - - let status = SecItemAdd(addQuery as CFDictionary, nil) - guard status == errSecSuccess else { - throw WalletStorageError.keychainError(status) - } - } - - public func retrieveSeedWithBiometric() throws -> Data { - let context = LAContext() - context.localizedReason = "Authenticate to access your wallet" - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: keychainService, - kSecAttrAccount as String: biometricKeychainAccount, - kSecReturnData as String: true, - kSecUseAuthenticationContext as String: context - ] - - var result: AnyObject? - let status = SecItemCopyMatching(query as CFDictionary, &result) - - guard status == errSecSuccess, - let seed = result as? Data else { - throw WalletStorageError.biometricAuthenticationFailed - } - - return seed - } - // MARK: - Legacy Cleanup /// Best-effort scrub of keychain residue from prior app @@ -574,8 +509,6 @@ public struct WalletKeychainMetadata: Codable, Equatable { public enum WalletStorageError: LocalizedError { case keychainError(OSStatus) case mnemonicNotFound - case biometricSetupFailed - case biometricAuthenticationFailed public var errorDescription: String? { switch self { @@ -583,10 +516,6 @@ public enum WalletStorageError: LocalizedError { return "Keychain error: \(status)" case .mnemonicNotFound: return "Mnemonic not found" - case .biometricSetupFailed: - return "Failed to setup biometric protection" - case .biometricAuthenticationFailed: - return "Biometric authentication failed" } } } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/FFI/KeychainSigner.swift b/packages/swift-sdk/Sources/SwiftDashSDK/FFI/KeychainSigner.swift index d41605004fd..f2cb1a3136c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/FFI/KeychainSigner.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/FFI/KeychainSigner.swift @@ -83,7 +83,7 @@ import SwiftData /// Result: every `KeychainSigner` instance leaked forever. The /// current `passUnretained` shape removes the leak at the cost /// of the explicit keepalive contract above. -public final class KeychainSigner: Signer, @unchecked Sendable { +public final class KeychainSigner: @unchecked Sendable { final class AdditionalSigningKeyEntry: @unchecked Sendable { let publicKey: Data private var privateKeyBytes: [UInt8] @@ -951,12 +951,6 @@ public final class KeychainSigner: Signer, @unchecked Sendable { return .failure(.ffiSignFailed(message: String(describing: error))) } } - - // MARK: - Signer protocol conformance (legacy) - - public func canSign(identityPublicKey: Data) -> Bool { - canSign(publicKey: identityPublicKey, keyType: KeyType.ecdsaSecp256k1.rawValue) - } } // MARK: - C-ABI trampolines diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/FFI/Signer.swift b/packages/swift-sdk/Sources/SwiftDashSDK/FFI/Signer.swift deleted file mode 100644 index 4bf682e1fc6..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/FFI/Signer.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -// MARK: - Signer Protocol - -/// Legacy Swift-side `Signer` protocol. -/// -/// `KeychainSigner` is the production implementation — it is also -/// what every FFI `_with_signer` entry point expects via its -/// `.handle` property. Signing itself happens entirely through that -/// handle (the FFI signing path); the protocol only exposes the -/// can-sign capability check. -/// -/// New code should depend on `KeychainSigner` directly and pass -/// `signer.handle` to FFI; this protocol does not (and cannot) -/// participate in the FFI signing path. -public protocol Signer: Sendable { - /// Check if this signer can sign for the given public key. - /// - Parameter identityPublicKey: The public key data to check. - /// - Returns: true if the signer has the corresponding private key. - func canSign(identityPublicKey: Data) -> Bool -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/FFI/StateTransitionExtensions.swift b/packages/swift-sdk/Sources/SwiftDashSDK/FFI/StateTransitionExtensions.swift index aacf891dd5e..fabd9489521 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/FFI/StateTransitionExtensions.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/FFI/StateTransitionExtensions.swift @@ -149,66 +149,6 @@ private func createPublicKeyHandle(from key: IdentityPublicKey, operation: Strin @MainActor extension SDK { - // MARK: - Helpers (nonisolated) - - /// JSON-encode a contract sub-payload off-actor for FFI hand-off. - /// Used by `dataContractCreate` to serialize document schemas, - /// token schemas, groups, keywords, and config — every payload - /// the FFI accepts as a JSON string. `allowEmpty: false` returns - /// `nil` for empty containers so callers can drop the parameter - /// entirely (the FFI treats a missing pointer as "field absent", - /// which lets the V1 deserializer fall back to its serde default). - nonisolated fileprivate static func encodeContractField( - _ value: Any, - fieldName: String, - allowEmpty: Bool - ) throws -> String? { - if !allowEmpty { - if let dict = value as? [String: Any], dict.isEmpty { return nil } - if let arr = value as? [Any], arr.isEmpty { return nil } - } - guard JSONSerialization.isValidJSONObject(value) else { - throw SDKError.serializationError("\(fieldName) is not JSON-serializable") - } - guard let data = try? JSONSerialization.data(withJSONObject: value), - let str = String(data: data, encoding: .utf8) else { - throw SDKError.serializationError("Failed to serialize \(fieldName)") - } - return str - } - - /// Run `body` with parallel C-string pointers for each input, - /// where `nil` Swift entries map to NULL pointers. The caller - /// receives an array `ptrs` whose `ptrs[i]` is either a valid - /// `UnsafePointer` for the duration of the call or - /// `nil`. Implemented recursively over `inputs.indices` so - /// every backing `String`'s lifetime extends through the - /// entire body — six nested `withCString` calls in source form - /// without the visual nesting. - nonisolated fileprivate static func withOptionalCStrings( - _ inputs: [String?], - _ body: ([UnsafePointer?]) -> R - ) -> R { - var collected: [UnsafePointer?] = Array(repeating: nil, count: inputs.count) - func step(_ index: Int) -> R { - if index == inputs.count { - return body(collected) - } - switch inputs[index] { - case .some(let s): - return s.withCString { ptr in - collected[index] = ptr - let result = step(index + 1) - collected[index] = nil - return result - } - case .none: - return step(index + 1) - } - } - return step(0) - } - // MARK: - Identity Handle Management /// Convert a DPPIdentity to an identity handle @@ -1382,17 +1322,6 @@ extension SDK { // MARK: - Token State Transitions - /// Transfer tokens between identities - public func tokenTransfer( - tokenId: String, - fromIdentityId: String, - toIdentityId: String, - amount: UInt64 - ) async throws -> (senderBalance: UInt64, receiverBalance: UInt64) { - // TODO: Implement when FFI binding is available - throw SDKError.notImplemented("Token transfer not yet implemented") - } - /// Mint new tokens public func tokenMint( contractId: String, diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Helpers/TestKeyGenerator.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Helpers/TestKeyGenerator.swift deleted file mode 100644 index 3e569e669ef..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Helpers/TestKeyGenerator.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Foundation -import CryptoKit - -/// Test key generator for demo purposes only -/// DO NOT USE IN PRODUCTION - This generates deterministic keys which are insecure -struct TestKeyGenerator { - - /// Generate a deterministic private key from identity ID (FOR DEMO ONLY) - static func generateTestPrivateKey(identityId: Data, keyIndex: UInt32, purpose: UInt8) -> Data { - // Create deterministic seed from identity ID, key index, and purpose - var seedData = Data() - seedData.append(identityId) - seedData.append(contentsOf: withUnsafeBytes(of: keyIndex) { Data($0) }) - seedData.append(purpose) - - // Use SHA256 to generate a 32-byte private key - let hash = SHA256.hash(data: seedData) - return Data(hash) - } - - /// Generate test private keys for an identity - static func generateTestPrivateKeys(identityId: Data) -> [String: Data] { - var keys: [String: Data] = [:] - - // Generate keys for different purposes - // Key 0: Master key (not used in state transitions) - keys["0"] = generateTestPrivateKey(identityId: identityId, keyIndex: 0, purpose: 0) - - // Key 1: Authentication key (HIGH security) - keys["1"] = generateTestPrivateKey(identityId: identityId, keyIndex: 1, purpose: 0) - - // Key 2: Transfer key (CRITICAL security, purpose 3 = TRANSFER) - keys["2"] = generateTestPrivateKey(identityId: identityId, keyIndex: 2, purpose: 3) - - // Key 3: Another transfer key (some identities might have transfer key at index 3) - keys["3"] = generateTestPrivateKey(identityId: identityId, keyIndex: 3, purpose: 3) - - return keys - } - - /// Get private key for a specific key ID - static func getPrivateKey(identityId: Data, keyId: UInt32) -> Data? { - let keys = generateTestPrivateKeys(identityId: identityId) - return keys[String(keyId)] - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentDataContract.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentDataContract.swift index 2fcc7c4ab4b..a125b695f7a 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentDataContract.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentDataContract.swift @@ -237,26 +237,9 @@ public final class PersistentDataContract { self.lastAccessedAt = Date() } - public func updateVersion(_ newVersion: Int) { - self.version = newVersion - self.lastUpdated = Date() - } - public func markAsSynced() { self.lastSyncedAt = Date() } - - public func addDocument(_ document: PersistentDocument) { - documents.append(document) - lastUpdated = Date() - } - - public func removeDocument(withId documentId: String) { - if let docIdData = Data.identifier(fromBase58: documentId) { - documents.removeAll { $0.id == docIdData } - } - lastUpdated = Date() - } } // MARK: - Queries diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentIdentity.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentIdentity.swift index 72e637a815c..3e25a32d33c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentIdentity.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentIdentity.swift @@ -273,29 +273,14 @@ public final class PersistentIdentity { self.lastUpdated = Date() } - public func updateRevision(_ newRevision: Int64) { - self.revision = newRevision - self.lastUpdated = Date() - } - public func markAsSynced() { self.lastSyncedAt = Date() } - public func updateDPNSName(_ name: String?) { - self.dpnsName = name - self.lastUpdated = Date() - } - public func addPublicKey(_ key: PersistentPublicKey) { publicKeys.append(key) lastUpdated = Date() } - - public func removePublicKey(withId keyId: Int32) { - publicKeys.removeAll { $0.keyId == keyId } - lastUpdated = Date() - } } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentToken.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentToken.swift index 5aaaa079aae..e170c387d8b 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentToken.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentToken.swift @@ -352,41 +352,4 @@ extension PersistentToken { token.contractId == contractId } } - - public static func tokensWithControlRulePredicate(rule: ControlRuleType) -> Predicate { - switch rule { - case .manualMinting: - return #Predicate { token in - token.manualMintingRules != nil - } - case .manualBurning: - return #Predicate { token in - token.manualBurningRules != nil - } - case .freeze: - return #Predicate { token in - token.freezeRules != nil - } - case .unfreeze: - return #Predicate { token in - token.unfreezeRules != nil - } - case .destroyFrozenFunds: - return #Predicate { token in - token.destroyFrozenFundsRules != nil - } - case .emergencyAction: - return #Predicate { token in - token.emergencyActionRules != nil - } - case .conventions: - return #Predicate { token in - token.conventionsChangeRules != nil - } - case .maxSupply: - return #Predicate { token in - token.maxSupplyChangeRules != nil - } - } - } } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Types/TokenTypes.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Types/TokenTypes.swift index 91443d2083e..30c0810e683 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Types/TokenTypes.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Types/TokenTypes.swift @@ -172,18 +172,6 @@ public enum TokenTradeMode: String, CaseIterable, Codable, Sendable { // MARK: - Control Rule Types -/// Types of control rules that can be configured on tokens -public enum ControlRuleType: Sendable { - case conventions - case maxSupply - case manualMinting - case manualBurning - case freeze - case unfreeze - case destroyFrozenFunds - case emergencyAction -} - /// Types of change control rules for token configuration public enum ChangeControlRuleType: Sendable { case conventions diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift index b0efd1a4093..9a7a7585cfa 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift @@ -33,8 +33,6 @@ public final class FinalizedCoreTransaction { return value } - func takeForAbandon() throws -> Handle { try takeForBroadcast() } - /// Consensus-serialized signed transaction bytes (copied out) without /// consuming the ownership token. public func serializedData() throws -> Data { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift index 8ce56359353..f70070b4db0 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift @@ -298,19 +298,6 @@ public class ManagedCoreWallet { } } - /// Compatibility wrapper preserving the former throwing API. - @available(*, deprecated, message: "Use broadcastTransactionWithOutcome(_:) and handle accepted/rejected/unknown") - public func broadcastTransaction(_ tx: FinalizedCoreTransaction) throws -> String { - switch try broadcastTransactionWithOutcome(tx) { - case .accepted(let txid): - return txid - case .rejected(_, let reason): - throw PlatformWalletError.transactionBroadcastRejected(reason) - case .unknown(_, let reason): - throw PlatformWalletError.transactionBroadcastUnconfirmed(reason) - } - } - /// Broadcast the deferred (BIP70/BIP270) payment behind `token` and return /// its txid. The token is consumed atomically before the send, so a repeated /// or concurrent broadcast gets an error rather than a second send. @@ -342,7 +329,7 @@ public class ManagedCoreWallet { public func abandonTransaction(_ tx: FinalizedCoreTransaction) throws { try core_wallet_abandon_signed_transaction( handle, - tx.takeForAbandon() + tx.takeForBroadcast() ).check() } } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/IdentityManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/IdentityManager.swift deleted file mode 100644 index a057c1a9003..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/IdentityManager.swift +++ /dev/null @@ -1,83 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Identity Manager for managing Platform identities. -/// -/// All FFI calls go through pointer-passing for identifiers — see the -/// `Identifier.withFFIBytes` extension. Out-buffer reads use a local -/// `[UInt8](repeating:count:32)` and copy into a `Data` value at -/// return time. -public class IdentityManager { - internal let handle: Handle - - internal init(handle: Handle) { - self.handle = handle - } - - deinit { - identity_manager_destroy(handle).discard() - } - - /// Create a new empty Identity Manager - public static func create() throws -> IdentityManager { - var handle: Handle = NULL_HANDLE - try identity_manager_create(&handle).check() - return IdentityManager(handle: handle) - } - - /// Add an identity to the manager - public func addIdentity(_ identity: ManagedIdentity) throws { - try identity_manager_add_identity(handle, identity.handle).check() - } - - /// Remove an identity from the manager - public func removeIdentity(_ identityId: Identifier) throws { - try identityId.withFFIBytes { idPtr in - try identity_manager_remove_identity(handle, idPtr).check() - } - } - - /// Get an identity by ID - public func getIdentity(_ identityId: Identifier) throws -> ManagedIdentity { - var identityHandle: Handle = NULL_HANDLE - try identityId.withFFIBytes { idPtr in - try identity_manager_get_identity(handle, idPtr, &identityHandle).check() - } - return ManagedIdentity(handle: identityHandle) - } - - /// Get all identity IDs - public func getAllIdentityIds() throws -> [Identifier] { - var array = IdentifierArray(items: nil, count: 0) - try identity_manager_get_all_identity_ids(handle, &array).check() - - defer { - platform_wallet_identifier_array_free(&array) - } - - guard array.items != nil, array.count > 0 else { - return [] - } - - var identifiers: [Identifier] = [] - identifiers.reserveCapacity(Int(array.count)) - for i in 0.. Int { - var count: UInt = 0 - try identity_manager_get_identity_count(handle, &count).check() - return Int(count) - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWallet.swift deleted file mode 100644 index 958010bdc5b..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWallet.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Platform Wallet for managing identities and DashPay contacts -public class PlatformWallet { - private let handle: Handle - private var identityManagers: [Network: IdentityManager] = [:] - - private init(handle: Handle) { - self.handle = handle - } - - deinit { - platform_wallet_info_destroy(handle).discard() - } - - /// Create a new Platform Wallet from a 64-byte seed - public static func fromSeed(_ seed: Data, network: Network = .testnet) throws -> PlatformWallet { - guard seed.count == 64 else { - throw PlatformWalletError.invalidParameter( - "seed must be 64 bytes, got \(seed.count)" - ) - } - - var handle: Handle = NULL_HANDLE - try seed.withUnsafeBytes { seedPtr in - try platform_wallet_info_create_from_seed( - network.ffiValue, - seedPtr.baseAddress?.assumingMemoryBound(to: UInt8.self), - UInt(seed.count), - &handle - ).check() - } - return PlatformWallet(handle: handle) - } - - /// Create a new Platform Wallet from a BIP39 mnemonic phrase - public static func fromMnemonic( - _ mnemonic: String, - network: Network = .testnet - ) throws -> PlatformWallet { - var handle: Handle = NULL_HANDLE - - let mnemonicCStr = (mnemonic as NSString).utf8String - - try platform_wallet_info_create_from_mnemonic( - network.ffiValue, - mnemonicCStr, - &handle - ).check() - - return PlatformWallet(handle: handle) - } - - /// Get the identity manager for a specific network - public func getIdentityManager(for network: Network) throws -> IdentityManager { - // Check if we already have it cached - if let manager = identityManagers[network] { - return manager - } - - var managerHandle: Handle = NULL_HANDLE - try platform_wallet_info_get_identity_manager(handle, &managerHandle).check() - - let manager = IdentityManager(handle: managerHandle) - identityManagers[network] = manager - return manager - } - - /// Set the identity manager for a specific network - public func setIdentityManager(_ manager: IdentityManager, for network: Network) throws { - try platform_wallet_info_set_identity_manager(handle, manager.handle).check() - identityManagers[network] = manager - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift index 49e97964602..7b03d879fdf 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift @@ -3005,14 +3005,6 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { } } - private func addDelta(_ base: UInt64, _ delta: Int64) -> UInt64 { - if delta >= 0 { - return base.addingReportingOverflow(UInt64(delta)).0 - } - let sub = UInt64(-delta) - return base >= sub ? base - sub : 0 - } - // MARK: - Callbacks /// Explicit semantic capability declaration passed alongside (not inside) @@ -4835,7 +4827,6 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { let readOnly: Bool let disabledAt: UInt64? let publicKeyData: Data - let publicKeyHash: Data /// Owning wallet if this key is derivable from one we control. let walletId: Data? /// DIP-9 `(identity_index, key_index)` pair. Present iff the key is @@ -5118,8 +5109,6 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { } } - // MARK: - Watch-only Restore: Wallet Metadata - // MARK: - Shielded persistence (Orchard) /// One incoming shielded-note row from @@ -5485,7 +5474,6 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { // future field additions safe.) let buf = UnsafeMutablePointer.allocate(capacity: rows.count) allocation.entries = buf - allocation.entriesCount = rows.count // `written` is the next free slot in `buf`; we increment it // only after a row's struct is fully populated, so the // returned prefix `[0..written)` is contiguous initialized @@ -5594,7 +5582,6 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { capacity: rows.count ) allocation.entries = buf - allocation.entriesCount = rows.count // Same `written`-counter discipline as `loadShieldedNotes`: // increment only after a slot is fully populated so the // returned prefix `[0..written)` is contiguous initialized @@ -5704,7 +5691,6 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { capacity: rows.count ) allocation.entries = buf - allocation.entriesCount = rows.count var written = 0 for row in rows { guard row.walletId.count == 32, row.entryId.count == 32 else { continue } @@ -5828,7 +5814,6 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { capacity: rows.count ) allocation.entries = buf - allocation.entriesCount = rows.count // Same `written`-counter pattern as `loadShieldedNotes`: // skip malformed rows without leaving holes in the // contiguous prefix Rust will read. @@ -5927,7 +5912,6 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { capacity: rows.count ) allocation.entries = buf - allocation.entriesCount = rows.count var written = 0 for row in rows { var entry = ShieldedViewingKeyRestoreFFI() @@ -6706,7 +6690,6 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { capacity: restorable.count ) allocation.entries = entriesPtr - allocation.entriesCount = restorable.count for (i, w) in restorable.enumerated() { let sortedAccounts = w.accounts @@ -6995,8 +6978,7 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { entriesPtr[i] = entry // Bump the initialized-count so a later abort path's // `release()` only deinitializes slots that were - // actually written (see `entriesInitialized`'s - // doc-comment for why we can't reuse `entriesCount`). + // actually written (see `entriesInitialized`'s doc). allocation.entriesInitialized = i + 1 } @@ -8538,17 +8520,12 @@ private final class TrackedMasternodeLoadAllocation { private final class LoadAllocation { var entries: UnsafeMutablePointer? - /// Allocated capacity — equal to `restorable.count`. Used for - /// `deallocate()` (which only requires "the original allocation - /// size") and as the upper bound on `entriesInitialized`. - var entriesCount: Int = 0 - /// How many of the `entriesCount` slots have actually been - /// written via `entriesPtr[i] = entry`. Tracked separately from - /// `entriesCount` because early-abort paths (account-tag + /// How many slots have actually been written via + /// `entriesPtr[i] = entry`. Early-abort paths (account-tag /// overflow, UTXO marshalling failure) call `release()` after - /// only `0...allocate`, freed by `deallocate()`. var cStringBuffers: [(UnsafeMutablePointer, Int)] = [] - /// `*const c_char` arrays referenced by `dpns_names` / - /// `contested_dpns_names`. Each inner pointer points into - /// `cStringBuffers`; releasing this array doesn't touch the - /// underlying strings. - var cStringPointerArrays: [(UnsafeMutablePointer?>, Int)] = [] /// Per-wallet `UtxoRestoreEntryFFI` arrays. The script bytes each /// row references live in `scalarBuffers`. var utxoArrays: [(UnsafeMutablePointer, Int)] = [] @@ -8630,8 +8602,7 @@ private final class LoadAllocation { func release() { if let entries = entries { // Deinitialize ONLY the slots that were actually written - // (`entriesInitialized`), then deallocate the full - // capacity (`entriesCount`). Per Swift's pointer + // (`entriesInitialized`), then deallocate. Per Swift's pointer // contract, `deinitialize(count:)` requires the region // to be initialized; `deallocate()` only requires the // pointer to match the original allocation. @@ -8681,9 +8652,6 @@ private final class LoadAllocation { for (ptr, _) in cStringBuffers { ptr.deallocate() } - for (ptr, _) in cStringPointerArrays { - ptr.deallocate() - } for (ptr, count) in utxoArrays { ptr.deinitialize(count: count) ptr.deallocate() @@ -8715,7 +8683,6 @@ private final class LoadAllocation { /// buffer plus per-row `note_data` byte buffers. private final class ShieldedLoadAllocation { var entries: UnsafeMutablePointer? - var entriesCount: Int = 0 var entriesInitialized: Int = 0 /// Per-row `note_data` byte buffers; each entry's /// `note_data_ptr` references one of these. @@ -8740,7 +8707,6 @@ private final class ShieldedLoadAllocation { /// of the `scalarBuffers`. private final class ShieldedOutgoingNoteLoadAllocation { var entries: UnsafeMutablePointer? - var entriesCount: Int = 0 var entriesInitialized: Int = 0 /// Per-row `memo` byte buffers; each entry's `memo_ptr` /// references one of these. @@ -8764,7 +8730,6 @@ private final class ShieldedOutgoingNoteLoadAllocation { /// entries buffer. private final class ShieldedSyncStateLoadAllocation { var entries: UnsafeMutablePointer? - var entriesCount: Int = 0 var entriesInitialized: Int = 0 func release() { @@ -8783,7 +8748,6 @@ private final class ShieldedSyncStateLoadAllocation { /// `ShieldedSyncStateLoadAllocation`. private final class ShieldedViewingKeyLoadAllocation { var entries: UnsafeMutablePointer? - var entriesCount: Int = 0 var entriesInitialized: Int = 0 func release() { @@ -8802,7 +8766,6 @@ private final class ShieldedViewingKeyLoadAllocation { /// entry's `*_ptr` references one of `scalarBuffers`. private final class ShieldedActivityLoadAllocation { var entries: UnsafeMutablePointer? - var entriesCount: Int = 0 var entriesInitialized: Int = 0 var scalarBuffers: [(UnsafeMutablePointer, Int)] = [] @@ -9494,7 +9457,6 @@ private func persistIdentityKeysCallback( readOnly: e.read_only, disabledAt: e.disabled_at_is_some ? e.disabled_at : nil, publicKeyData: pubKey, - publicKeyHash: dataFromTuple20(e.public_key_hash), walletId: walletId, derivationIndices: indices, contractBounds: bounds @@ -9954,15 +9916,6 @@ private func dataFromTuple32(_ tuple: FFIByteTuple32) -> Data { return Swift.withUnsafeBytes(of: &value) { Data($0) } } -/// Copy a fixed 20-byte C tuple into an owned `Data`. Identical -/// idiom to `dataFromTuple32`, just for RIPEMD160(SHA256) pubkey -/// hashes on identity-key entries. -@inline(__always) -private func dataFromTuple20(_ tuple: FFIByteTuple20) -> Data { - var value = tuple - return Swift.withUnsafeBytes(of: &value) { Data($0) } -} - private func persistWalletMetadataCallback( context: UnsafeMutableRawPointer?, walletIdPtr: UnsafePointer?, diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/README.md b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/README.md deleted file mode 100644 index a08b9d2bc7d..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/README.md +++ /dev/null @@ -1,600 +0,0 @@ -# Platform Wallet API Documentation - -## Overview - -The Platform Wallet module provides Swift bindings for managing Dash Platform identities and DashPay contacts. It wraps the Rust FFI layer to provide a memory-safe, Swift-idiomatic API. - -## Quick Start - -```swift -import SwiftDashSDK - -// Create a Platform Wallet from mnemonic -let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" -let wallet = try PlatformWallet.fromMnemonic(mnemonic) - -// Get identity manager for testnet -let identityManager = try wallet.getIdentityManager(for: .testnet) - -// Check identity count -let count = try identityManager.getIdentityCount() -print("Total identities: \(count)") -``` - -## Core Components - -### PlatformWallet - -The main entry point for Platform Wallet functionality. - -#### Initialization - -**From Mnemonic:** -```swift -static func fromMnemonic(_ mnemonic: String, passphrase: String? = nil) throws -> PlatformWallet -``` - -Creates a Platform Wallet from a BIP39 mnemonic phrase with optional passphrase. - -Example: -```swift -let wallet = try PlatformWallet.fromMnemonic("word1 word2 ... word12") -let walletWithPassphrase = try PlatformWallet.fromMnemonic( - "word1 word2 ... word12", - passphrase: "my-secret-passphrase" -) -``` - -**From Seed:** -```swift -static func fromSeed(_ seed: Data) throws -> PlatformWallet -``` - -Creates a Platform Wallet from a 64-byte seed. - -Example: -```swift -let seed = Data(count: 64) // Your seed bytes -let wallet = try PlatformWallet.fromSeed(seed) -``` - -#### Identity Manager Access - -```swift -func getIdentityManager(for network: Network) throws -> IdentityManager -``` - -Gets or creates an identity manager for a specific network. Results are cached per network. - -Example: -```swift -let mainnetManager = try wallet.getIdentityManager(for: .mainnet) -let testnetManager = try wallet.getIdentityManager(for: .testnet) -``` - -```swift -func setIdentityManager(_ manager: IdentityManager, for network: Network) throws -``` - -Sets a specific identity manager for a network. - ---- - -### IdentityManager - -Manages a collection of identities for a specific network. - -#### Identity Management - -**Create Manager:** -```swift -static func create() throws -> IdentityManager -``` - -**Add Identity:** -```swift -func addIdentity(_ identity: ManagedIdentity) throws -``` - -**Get Identity:** -```swift -func getIdentity(_ identityId: Identifier) throws -> ManagedIdentity -``` - -**Remove Identity:** -```swift -func removeIdentity(_ identityId: Identifier) throws -``` - -Example: -```swift -let manager = try IdentityManager.create() - -// Add an identity -let identity = try ManagedIdentity.fromIdentityBytes(identityBytes) -try manager.addIdentity(identity) - -// Get it back -let retrievedIdentity = try manager.getIdentity(identityId) - -// Remove it -try manager.removeIdentity(identityId) -``` - -#### Query Operations - -**Get All Identity IDs:** -```swift -func getAllIdentityIds() throws -> [Identifier] -``` - -**Get Identity Count:** -```swift -func getIdentityCount() throws -> Int -``` - -**Primary Identity:** -```swift -func getPrimaryIdentityId() throws -> Identifier? -func setPrimaryIdentity(_ identityId: Identifier) throws -``` - -Example: -```swift -// List all identities -let allIds = try manager.getAllIdentityIds() -print("Found \(allIds.count) identities") - -// Set primary identity -try manager.setPrimaryIdentity(allIds[0]) - -// Get primary identity -if let primaryId = try manager.getPrimaryIdentityId() { - let primaryIdentity = try manager.getIdentity(primaryId) -} -``` - ---- - -### ManagedIdentity - -Represents a Platform identity with DashPay contact metadata. - -#### Creation - -```swift -static func fromIdentityBytes(_ bytes: Data) throws -> ManagedIdentity -``` - -Creates a ManagedIdentity from serialized DPP identity bytes. - -#### Identity Information - -```swift -func getId() throws -> Identifier -func getBalance() throws -> UInt64 -func getLabel() throws -> String? -func setLabel(_ label: String) throws -``` - -Example: -```swift -let id = try identity.getId() -let balance = try identity.getBalance() -print("Identity \(id.hexString) has \(balance) credits") - -try identity.setLabel("My Main Identity") -``` - -#### Block Time Tracking - -```swift -func getLastUpdatedBalanceBlockTime() throws -> BlockTime? -func setLastUpdatedBalanceBlockTime(_ blockTime: BlockTime) throws -func getLastSyncedKeysBlockTime() throws -> BlockTime? -``` - -#### Contact Requests - -**Send Contact Request:** -```swift -func sendContactRequest( - recipientId: Identifier, - senderKeyIndex: UInt32, - recipientKeyIndex: UInt32, - accountReference: UInt32, - encryptedPublicKey: Data -) throws -``` - -Example: -```swift -let recipientId = try Identifier(hexString: "abcd...") -let encryptedKey = // ... ECDH encrypted public key - -try identity.sendContactRequest( - recipientId: recipientId, - senderKeyIndex: 0, - recipientKeyIndex: 0, - accountReference: 0, - encryptedPublicKey: encryptedKey -) -``` - -**Accept / Ignore Requests:** -```swift -func acceptContactRequest(senderId: Identifier) throws -func ignoreContactSender(senderId: Identifier) throws // per-sender, reversible -func unignoreContactSender(senderId: Identifier) throws -``` - -**Query Contact Requests:** -```swift -func getSentContactRequestIds() throws -> [Identifier] -func getIncomingContactRequestIds() throws -> [Identifier] -func getSentContactRequest(recipientId: Identifier) throws -> ContactRequest? -func getIncomingContactRequest(senderId: Identifier) throws -> ContactRequest? -``` - -Example: -```swift -// Get all incoming requests -let incomingIds = try identity.getIncomingContactRequestIds() -for senderId in incomingIds { - if let request = try identity.getIncomingContactRequest(senderId: senderId) { - let sender = try request.getSenderId() - print("Request from \(sender.hexString)") - - // Accept or ignore - try identity.acceptContactRequest(senderId: senderId) - } -} -``` - -#### Established Contacts - -```swift -func getEstablishedContactIds() throws -> [Identifier] -func getEstablishedContact(contactId: Identifier) throws -> EstablishedContact? -func isContactEstablished(contactId: Identifier) throws -> Bool -``` - -Example: -```swift -// List all contacts -let contactIds = try identity.getEstablishedContactIds() - -for contactId in contactIds { - if let contact = try identity.getEstablishedContact(contactId: contactId) { - let alias = try contact.getAlias() - print("Contact: \(alias ?? contactId.hexString)") - } -} -``` - ---- - -### ContactRequest - -Represents a contact request between two identities. - -#### Creation - -```swift -static func create( - senderId: Identifier, - recipientId: Identifier, - senderKeyIndex: UInt32, - recipientKeyIndex: UInt32, - accountReference: UInt32, - encryptedPublicKey: Data, - createdAt: UInt64 -) throws -> ContactRequest -``` - -#### Properties - -```swift -func getSenderId() throws -> Identifier -func getRecipientId() throws -> Identifier -func getSenderKeyIndex() throws -> UInt32 -func getRecipientKeyIndex() throws -> UInt32 -func getAccountReference() throws -> UInt32 -func getEncryptedPublicKey() throws -> Data -func getCreatedAt() throws -> UInt64 -``` - -Example: -```swift -let senderId = try request.getSenderId() -let recipientId = try request.getRecipientId() -let encryptedKey = try request.getEncryptedPublicKey() -let timestamp = try request.getCreatedAt() - -print("Request from \(senderId.hexString) to \(recipientId.hexString)") -print("Created at: \(Date(timeIntervalSince1970: Double(timestamp) / 1000))") -``` - ---- - -### EstablishedContact - -Represents a bidirectional friendship in DashPay. - -#### Contact Information - -```swift -func getContactIdentityId() throws -> Identifier -``` - -#### Alias Management - -```swift -func getAlias() throws -> String? -func setAlias(_ alias: String) throws -func clearAlias() throws -``` - -Example: -```swift -// Set a friendly name -try contact.setAlias("Alice") - -// Get the alias -if let alias = try contact.getAlias() { - print("Contact name: \(alias)") -} - -// Clear it -try contact.clearAlias() -``` - -#### Notes - -```swift -func getNote() throws -> String? -func setNote(_ note: String) throws -func clearNote() throws -``` - -Example: -```swift -try contact.setNote("Met at conference 2024") -let note = try contact.getNote() -try contact.clearNote() -``` - -#### Visibility - -```swift -func isHidden() throws -> Bool -func hide() throws -func unhide() throws -``` - -Example: -```swift -// Hide contact -try contact.hide() -print("Is hidden: \(try contact.isHidden())") - -// Show contact again -try contact.unhide() -``` - ---- - -## Supporting Types - -### Identifier - -32-byte identifier for identities and documents. - -```swift -struct Identifier { - let bytes: [UInt8] - var hexString: String - - init(bytes: [UInt8]) throws - init(hexString: String) throws - static func random() throws -> Identifier -} -``` - -Example: -```swift -// From hex string -let id = try Identifier(hexString: "abcd1234...") - -// From bytes -let bytes: [UInt8] = [0x01, 0x02, ...] -let id2 = try Identifier(bytes: bytes) - -// Generate random -let randomId = try Identifier.random() - -// Convert to hex -print(randomId.hexString) -``` - -### BlockTime - -Platform block information. - -```swift -struct BlockTime { - let height: UInt32 - let coreHeight: UInt32 - let timestamp: UInt64 - - init(height: UInt32, coreHeight: UInt32, timestamp: UInt64) -} -``` - -### Network - -Available network types. - -```swift -enum Network: UInt32 { - case mainnet = 0 - case testnet = 1 - case devnet = 2 - case local = 3 -} -``` - -### PlatformWalletError - -Error types thrown by Platform Wallet operations. - -```swift -enum PlatformWalletError: Error { - case nullPointer - case invalidHandle - case invalidParameter - case invalidIdentifier - case invalidNetwork - case walletOperation(String) - case identityNotFound - case contactNotFound - case utf8Conversion - case serialization - case deserialization - case unknown(String) -} -``` - ---- - -## Usage Patterns - -### Complete Contact Request Flow - -```swift -// Alice sends request to Bob -let aliceIdentity = try ManagedIdentity.fromIdentityBytes(aliceBytes) -let bobId = try Identifier(hexString: "bob-id-hex") - -try aliceIdentity.sendContactRequest( - recipientId: bobId, - senderKeyIndex: 0, - recipientKeyIndex: 0, - accountReference: 0, - encryptedPublicKey: encryptedKey -) - -// Bob receives and accepts -let bobIdentity = try ManagedIdentity.fromIdentityBytes(bobBytes) -let aliceId = try Identifier(hexString: "alice-id-hex") - -// Check for request -if let request = try bobIdentity.getIncomingContactRequest(senderId: aliceId) { - // Accept it - try bobIdentity.acceptContactRequest(senderId: aliceId) - - // Now they're contacts! - let isEstablished = try bobIdentity.isContactEstablished(contactId: aliceId) - print("Contact established: \(isEstablished)") -} -``` - -### Managing Contact Metadata - -```swift -let contacts = try identity.getEstablishedContactIds() - -for contactId in contacts { - if let contact = try identity.getEstablishedContact(contactId: contactId) { - // Set alias and note - try contact.setAlias("Alice Smith") - try contact.setNote("Friend from university") - - // Later, hide temporarily - try contact.hide() - - // Check visibility - let isVisible = !(try contact.isHidden()) - } -} -``` - -### Multi-Network Identity Management - -```swift -let wallet = try PlatformWallet.fromMnemonic(mnemonic) - -// Separate managers for each network -let mainnetManager = try wallet.getIdentityManager(for: .mainnet) -let testnetManager = try wallet.getIdentityManager(for: .testnet) - -// Add identities to appropriate networks -try testnetManager.addIdentity(testIdentity) -try mainnetManager.addIdentity(mainnetIdentity) - -// Set primary identity per network -try testnetManager.setPrimaryIdentity(testIdentityId) -try mainnetManager.setPrimaryIdentity(mainnetIdentityId) -``` - ---- - -## Memory Management - -All classes (PlatformWallet, IdentityManager, ManagedIdentity, ContactRequest, EstablishedContact) automatically manage their FFI handles through Swift's `deinit`. You don't need to manually free resources. - -```swift -do { - let wallet = try PlatformWallet.fromMnemonic(mnemonic) - let manager = try wallet.getIdentityManager(for: .testnet) - // Use manager... -} // wallet and manager are automatically freed here -``` - ---- - -## Thread Safety - -Most operations are synchronous and not inherently thread-safe. Use appropriate synchronization when accessing from multiple threads: - -```swift -actor PlatformWalletActor { - let wallet: PlatformWallet - - init(mnemonic: String) throws { - self.wallet = try PlatformWallet.fromMnemonic(mnemonic) - } - - func getManager(for network: Network) throws -> IdentityManager { - try wallet.getIdentityManager(for: network) - } -} -``` - ---- - -## Error Handling - -All throwing functions use Swift's error handling. Always wrap in `do-catch`: - -```swift -do { - let wallet = try PlatformWallet.fromMnemonic(mnemonic) - let manager = try wallet.getIdentityManager(for: .testnet) - let count = try manager.getIdentityCount() -} catch PlatformWalletError.invalidParameter { - print("Invalid input") -} catch PlatformWalletError.identityNotFound { - print("Identity not found") -} catch { - print("Other error: \(error)") -} -``` - ---- - -## See Also - -- [SwiftExampleApp Integration](../../../SwiftExampleApp/SwiftExampleApp/Services/DashPayService.swift) - Real-world usage example -- [Unit Tests](../../../SwiftTests/SwiftDashSDKTests/PlatformWalletTests.swift) - Comprehensive test examples -- [Integration Tests](../../../SwiftTests/SwiftDashSDKTests/PlatformWalletIntegrationTests.swift) - Full workflow examples diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swift index df1c44bfead..c5c4fdd3896 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swift @@ -61,7 +61,7 @@ public final class KeychainManager: Sendable { /// /// `nonisolated` so the singleton can be reached from off-actor /// contexts — e.g. the Rust persister callback path that writes - /// via `storePrivateKeyNonisolated`. Safe because + /// via `storePrivateKey`. Safe because /// `KeychainManager` is `Sendable` (all state is `let` + /// thread-safe Security-framework calls). public nonisolated static let shared = KeychainManager() @@ -105,31 +105,14 @@ public final class KeychainManager: Sendable { /// - identityId: The identity ID (32 bytes) /// - keyIndex: The key index within the identity /// - Returns: A unique identifier for the stored key, or nil if storage failed - @discardableResult - public func storePrivateKey(_ keyData: Data, identityId: Data, keyIndex: Int32) -> String? { - // Delegate to the nonisolated implementation so both the - // main-actor path and off-actor callers (e.g. Rust-side - // persister callbacks) share identical Keychain semantics. - return storePrivateKeyNonisolated(keyData, identityId: identityId, keyIndex: keyIndex) - } - - /// Off-actor variant of [`storePrivateKey`]. /// - /// The underlying Security-framework APIs (`SecItemAdd`, - /// `SecItemDelete`) are thread-safe, and this class's state - /// (`serviceName` + `accessGroup`) is immutable (`let`), so the - /// write can run from any isolation domain. This is the entry - /// point the FFI persister callback (`persistIdentityKeys` - /// in `PlatformWalletPersistenceHandler`) calls when Rust - /// forwards a `Clear` private key — the callback runs on the - /// Rust persister thread, not on the main actor, so the - /// `@MainActor`-pinned `storePrivateKey` isn't reachable. - /// - /// Prefer the `@MainActor` wrapper when you're already in a - /// main-actor context; call this directly from nonisolated - /// contexts (background queues, detached tasks, C callbacks). + /// `nonisolated`: the underlying Security-framework APIs + /// (`SecItemAdd`, `SecItemDelete`) are thread-safe and this class's + /// state (`serviceName` + `accessGroup`) is immutable (`let`), so the + /// write can run from any isolation domain — including off-actor + /// callers such as C callbacks on the Rust persister thread. @discardableResult - public nonisolated func storePrivateKeyNonisolated( + public nonisolated func storePrivateKey( _ keyData: Data, identityId: Data, keyIndex: Int32 @@ -229,11 +212,6 @@ public final class KeychainManager: Sendable { return status == errSecSuccess || status == errSecItemNotFound } - /// Delete every `privkey__*` keychain row for `identityId`. - public nonisolated func deleteAllPrivateKeys(for identityId: Data) throws { - try deleteItems(accountPrefixes: ["privkey_\(identityId.toHexString())_"]) - } - /// Delete every per-identity keychain row — both `privkey_*` and /// `specialkey_*` schemes — for `identityId`. public nonisolated func deleteAllKeychainItems(forIdentityId identityId: Data) throws { @@ -299,17 +277,6 @@ public final class KeychainManager: Sendable { return retrieveKeyData(identifier: keyIdentifier) } - /// Delete a special key from the keychain - /// - Parameters: - /// - identityId: The identity ID (32 bytes) - /// - keyType: The type of special key - /// - Returns: true if deletion succeeded or key didn't exist - @discardableResult - public func deleteSpecialKey(identityId: Data, keyType: SpecialKeyType) -> Bool { - let keyIdentifier = generateSpecialKeyIdentifier(identityId: identityId, keyType: keyType) - return deleteKeyData(identifier: keyIdentifier) - } - // MARK: - Key Existence Check /// Check if a private key exists in the keychain @@ -335,29 +302,6 @@ public final class KeychainManager: Sendable { return status == errSecSuccess } - /// Check if a special key exists in the keychain - /// - Parameters: - /// - identityId: The identity ID (32 bytes) - /// - keyType: The type of special key - /// - Returns: true if the key exists - public func hasSpecialKey(identityId: Data, keyType: SpecialKeyType) -> Bool { - let keyIdentifier = generateSpecialKeyIdentifier(identityId: identityId, keyType: keyType) - - var query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: serviceName, - kSecAttrAccount as String: keyIdentifier, - kSecMatchLimit as String: kSecMatchLimitOne - ] - - if let accessGroup = accessGroup { - query[kSecAttrAccessGroup as String] = accessGroup - } - - let status = SecItemCopyMatching(query as CFDictionary, nil) - return status == errSecSuccess - } - // MARK: - Generic Key Storage /// Store arbitrary data in the keychain with a custom identifier @@ -391,7 +335,7 @@ public final class KeychainManager: Sendable { /// `nonisolated` so the FFI signer trampoline (which runs from /// any Tokio worker, off the main actor) can call it directly. /// `SecItemCopyMatching` is thread-safe and this type's state is - /// `let` — same rationale as `storePrivateKeyNonisolated`. + /// `let` — same rationale as `storePrivateKey`. /// - Parameter identifier: The identifier for the stored data /// - Returns: The stored data, or nil if not found public nonisolated func retrieveKeyData(identifier: String) -> Data? { @@ -454,7 +398,7 @@ public final class KeychainManager: Sendable { /// Nonisolated because the result only depends on the arguments /// — no access to actor-isolated state — and the function is /// shared between the `@MainActor` wrapper methods and the - /// off-actor `storePrivateKeyNonisolated` path. + /// off-actor `storePrivateKey` path. private nonisolated func generateKeyIdentifier(identityId: Data, keyIndex: Int32) -> String { return "privkey_\(identityId.toHexString())_\(keyIndex)" } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/SwiftDashSDK.swift b/packages/swift-sdk/Sources/SwiftDashSDK/SwiftDashSDK.swift index ca3e4b2dbfa..99eaf81b585 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/SwiftDashSDK.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/SwiftDashSDK.swift @@ -1,5 +1,2 @@ // Re-export all C types so they're available to clients @_exported import DashSDKFFI - -public typealias ErrorCode = DashSDKErrorCode -public typealias SDKConfig = DashSDKConfig diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/IdentityManagerTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/IdentityManagerTests.swift deleted file mode 100644 index 8b781c91095..00000000000 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/IdentityManagerTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -import XCTest -@testable import SwiftDashSDK - -class IdentityManagerTests: XCTestCase { - - var identityManager: IdentityManager! - - override func setUpWithError() throws { - try super.setUpWithError() - identityManager = try IdentityManager.create() - } - - // MARK: - Identity Getters Tests - - func testInitialIdentityGetters() throws { - let count = try identityManager.getIdentityCount() - XCTAssertEqual(count, 0, "New manager should have 0 identities") - - let ids = try identityManager.getAllIdentityIds() - XCTAssertEqual(ids.count, 0, "New manager should have no identities") - } -} diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletIntegrationTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletIntegrationTests.swift deleted file mode 100644 index a821a0821fe..00000000000 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletIntegrationTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -import XCTest -@testable import SwiftDashSDK - -/// Integration tests for Platform Wallet with real identity data and contact flows -/// These tests require the full FFI stack to be built and linked -class PlatformWalletIntegrationTests: XCTestCase { - - var wallet: PlatformWallet! - var identityManager: IdentityManager! - - override func setUpWithError() throws { - try super.setUpWithError() - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - wallet = try PlatformWallet.fromMnemonic(mnemonic) - identityManager = try wallet.getIdentityManager(for: .testnet) - } - - // MARK: - Wallet and Identity Manager Integration - - func testWalletToIdentityManagerFlow() throws { - // Verify we can create wallet and get identity manager - XCTAssertNotNil(wallet) - XCTAssertNotNil(identityManager) - - // Verify initial state - let count = try identityManager.getIdentityCount() - XCTAssertGreaterThanOrEqual(count, 0, "Should have zero or more identities") - - let ids = try identityManager.getAllIdentityIds() - XCTAssertEqual(ids.count, count, "ID count should match identity count") - } - - func testMultipleNetworkIdentityManagers() throws { - let mainnetManager = try wallet.getIdentityManager(for: .mainnet) - let testnetManager = try wallet.getIdentityManager(for: .testnet) - let devnetManager = try wallet.getIdentityManager(for: .devnet) - - XCTAssertNotEqual(mainnetManager.handle, testnetManager.handle) - XCTAssertNotEqual(testnetManager.handle, devnetManager.handle) - XCTAssertNotEqual(mainnetManager.handle, devnetManager.handle) - } - - // MARK: - Error Handling Integration - - func testWalletCreationErrorHandling() { - // Test invalid mnemonic - XCTAssertThrowsError(try PlatformWallet.fromMnemonic("invalid mnemonic phrase")) { error in - XCTAssertTrue(error is PlatformWalletError) - } - - // Test invalid seed size - let invalidSeed = Data(count: 10) - XCTAssertThrowsError(try PlatformWallet.fromSeed(invalidSeed)) { error in - if case PlatformWalletError.invalidParameter = error { - // Expected - } else { - XCTFail("Expected invalidParameter error, got \(error)") - } - } - } - -} diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletTests.swift deleted file mode 100644 index b1c5c6ea721..00000000000 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -import XCTest -@testable import SwiftDashSDK - -class PlatformWalletTests: XCTestCase { - - var testSeed: Data! - var testMnemonic: String! - - override func setUp() { - super.setUp() - - // Create a 64-byte test seed - testSeed = Data(count: 64) - - // Use a valid BIP39 mnemonic (12 words) - testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - } - - // MARK: - Wallet Creation Tests - - func testCreateWalletFromSeed() throws { - let wallet = try PlatformWallet.fromSeed(testSeed) - XCTAssertNotNil(wallet, "Wallet should be created from seed") - } - - func testCreateWalletFromInvalidSeed() { - let invalidSeed = Data(count: 32) // Wrong size - - XCTAssertThrowsError(try PlatformWallet.fromSeed(invalidSeed)) { error in - XCTAssertTrue(error is PlatformWalletError) - if case PlatformWalletError.invalidParameter = error { - // Expected error - } else { - XCTFail("Expected invalidParameter error, got \(error)") - } - } - } - - func testCreateWalletFromMnemonic() throws { - let wallet = try PlatformWallet.fromMnemonic(testMnemonic) - XCTAssertNotNil(wallet, "Wallet should be created from mnemonic") - } - - // MARK: - Identity Manager Tests - - func testGetIdentityManager() throws { - let wallet = try PlatformWallet.fromSeed(testSeed) - let manager = try wallet.getIdentityManager(for: .testnet) - XCTAssertNotNil(manager, "Should get identity manager for testnet") - } - - func testGetIdentityManagerCaching() throws { - let wallet = try PlatformWallet.fromSeed(testSeed) - let manager1 = try wallet.getIdentityManager(for: .testnet) - let manager2 = try wallet.getIdentityManager(for: .testnet) - - // Should return the same cached instance - XCTAssertEqual(manager1.handle, manager2.handle, "Should return cached manager") - } - - func testSetIdentityManager() throws { - let wallet = try PlatformWallet.fromSeed(testSeed) - let newManager = try IdentityManager.create() - - try wallet.setIdentityManager(newManager, for: .mainnet) - let retrievedManager = try wallet.getIdentityManager(for: .mainnet) - - XCTAssertEqual(newManager.handle, retrievedManager.handle, "Should retrieve set manager") - } - - func testMultipleNetworkManagers() throws { - let wallet = try PlatformWallet.fromSeed(testSeed) - - let mainnetManager = try wallet.getIdentityManager(for: .mainnet) - let testnetManager = try wallet.getIdentityManager(for: .testnet) - let devnetManager = try wallet.getIdentityManager(for: .devnet) - - XCTAssertNotEqual(mainnetManager.handle, testnetManager.handle, "Different networks should have different managers") - XCTAssertNotEqual(testnetManager.handle, devnetManager.handle, "Different networks should have different managers") - } -}