From f131820eaa23c5725d6c64a6c0a8e961f2a7a9d0 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 9 Sep 2026 12:58:07 +0200 Subject: [PATCH 01/11] refactor(swift-example-app): remove the wallet PIN that was collected and validated but never used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `walletPin` / `confirmPin` gated `canCreateWallet` and a 4-6 digit guard, and were echoed by `print("PIN length: …")` — and that was the whole of it. The PIN never reached `createWallet`, `WalletStorage` or the Keychain, while the section footer promised "Choose a PIN to secure your wallet". In a QA app the team uses to validate wallet flows, that is misleading security UX. From the same file: the `.alert("Wallet Created", isPresented: .constant(false))` could never present (unchanged since 872383bba4). The `SwiftExampleAppUITests` UI test no longer fills the removed fields, and the `secureTextField` helper has no users left. Co-Authored-By: Claude Opus 5 --- .../Core/Views/CreateWalletView.swift | 53 ++----------------- .../SwiftExampleAppUITests.swift | 19 ------- 2 files changed, 4 insertions(+), 68 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift index 3af077393ad..ca67f67dac7 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift @@ -13,8 +13,6 @@ struct CreateWalletView: View { @State private var showImportOption: Bool = false @State private var importMnemonic: String = "" @State private var importBirthHeight: String = "" - @State private var walletPin: String = "" - @State private var confirmPin: String = "" @State private var isCreating: Bool = false @State private var error: Error? = nil @FocusState private var focusedField: Field? @@ -32,8 +30,6 @@ struct CreateWalletView: View { enum Field: Hashable { case walletName - case pin - case confirmPin case mnemonic } @@ -60,10 +56,10 @@ struct CreateWalletView: View { TextField("Wallet Name", text: $walletLabel) .textInputAutocapitalization(.words) .focused($focusedField, equals: .walletName) - .submitLabel(.next) + .submitLabel(.done) .accessibilityIdentifier("createWallet.walletNameField") .onSubmit { - focusedField = .pin + focusedField = nil } } header: { Text("Wallet Information") @@ -133,34 +129,6 @@ struct CreateWalletView: View { Text("Select which networks to create wallets for. The same seed will be used for all selected networks.") } - Section { - HStack { - Text("PIN:") - .frame(width: 100, alignment: .leading) - SecureField("4-6 digits", text: $walletPin) - .keyboardType(.numberPad) - .textContentType(.oneTimeCode) - .autocorrectionDisabled() - .focused($focusedField, equals: .pin) - .accessibilityIdentifier("createWallet.pinField") - } - - HStack { - Text("Confirm PIN:") - .frame(width: 100, alignment: .leading) - SecureField("4-6 digits", text: $confirmPin) - .keyboardType(.numberPad) - .textContentType(.oneTimeCode) - .autocorrectionDisabled() - .focused($focusedField, equals: .confirmPin) - .accessibilityIdentifier("createWallet.confirmPinField") - } - } header: { - Text("Security") - } footer: { - Text("Choose a PIN to secure your wallet (4-6 digits)") - } - Section { Toggle("Import Existing Wallet", isOn: $showImportOption) } header: { @@ -225,11 +193,6 @@ struct CreateWalletView: View { } } .disabled(isCreating) - .alert("Wallet Created", isPresented: .constant(false)) { - Button("OK") { } - } message: { - Text("Wallet created successfully") - } .alert("Error", isPresented: .constant(error != nil)) { Button("OK") { error = nil @@ -255,8 +218,6 @@ struct CreateWalletView: View { private var canCreateWallet: Bool { !walletLabel.isEmpty && - !walletPin.isEmpty && - walletPin == confirmPin && !isCreating && hasNetworkSelected } @@ -304,13 +265,8 @@ struct CreateWalletView: View { } private func createWallet(using mnemonic: String) { - guard !walletLabel.isEmpty, - walletPin == confirmPin, - walletPin.count >= 4 && walletPin.count <= 6 else { - print("=== WALLET CREATION VALIDATION FAILED ===") - print("Label empty: \(walletLabel.isEmpty)") - print("PINs match: \(walletPin == confirmPin)") - print("PIN length valid: \(walletPin.count >= 4 && walletPin.count <= 6)") + guard !walletLabel.isEmpty else { + print("=== WALLET CREATION VALIDATION FAILED: label empty ===") return } @@ -321,7 +277,6 @@ struct CreateWalletView: View { print("=== STARTING WALLET CREATION ===") let mnemonicPhrase = (showImportOption ? importMnemonic : mnemonic) - print("PIN length: \(walletPin.count)") print("Import option enabled: \(showImportOption)") let selectedNetworks: [Network] = [ diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/SwiftExampleAppUITests.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/SwiftExampleAppUITests.swift index a654b138360..74f8f1463f3 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/SwiftExampleAppUITests.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/SwiftExampleAppUITests.swift @@ -14,8 +14,6 @@ final class SwiftExampleAppUITests: XCTestCase { static let addWalletButton = "wallets.addWalletButton" static let emptyCreateWalletButton = "wallets.empty.createWalletButton" static let walletNameField = "createWallet.walletNameField" - static let pinField = "createWallet.pinField" - static let confirmPinField = "createWallet.confirmPinField" static let createWalletButton = "createWallet.createButton" static let wroteItDownToggle = "seedBackup.wroteItDownToggle" static let confirmSeedCreateWalletButton = "seedBackup.createWalletButton" @@ -111,16 +109,6 @@ final class SwiftExampleAppUITests: XCTestCase { walletNameField.tap() walletNameField.typeText(walletName) - let pinField = secureTextField(Identifier.pinField, in: app) - XCTAssertTrue(pinField.waitForExistence(timeout: 5)) - pinField.tap() - pinField.typeText("1234") - - let confirmPinField = secureTextField(Identifier.confirmPinField, in: app) - XCTAssertTrue(confirmPinField.waitForExistence(timeout: 5)) - confirmPinField.tap() - confirmPinField.typeText("1234") - let createButton = button(Identifier.createWalletButton, in: app) XCTAssertTrue( waitForElementToBeEnabled(createButton, timeout: 5), @@ -259,13 +247,6 @@ final class SwiftExampleAppUITests: XCTestCase { .firstMatch } - @MainActor - private func secureTextField(_ identifier: String, in app: XCUIApplication) -> XCUIElement { - app.secureTextFields - .matching(identifier: identifier) - .firstMatch - } - @MainActor private func switchControl(_ identifier: String, in app: XCUIApplication) -> XCUIElement { app.switches From 3380df56578513a760765c92d10df48237c9fae5 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 9 Sep 2026 12:58:18 +0200 Subject: [PATCH 02/11] refactor(swift-example-app): remove the unreachable PIN prompt flow and legacy SyncProgressRow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `showingPINPrompt = true` appears nowhere in the app, so the `.sheet(isPresented: $showingPINPrompt) { PINPromptView(…) }` and the 44-line `PINPromptView` are unreachable. `derivePrivateKeyWithPIN()` is a stub that only sets a "not yet available" message. Going with them: the unread `@State` vars `showingPrivateKey`, `privateKeyToShow`, `pinInput` and `copiedText` (AccountDetailView), plus `copiedText` in `ProofDetailView`. `struct SyncProgressRow` — marked "(Legacy)" in the source itself — has no reference outside its own definition. About 150 lines of unreachable UI out of the app's two largest view files. Co-Authored-By: Claude Opus 5 --- .../Core/Views/AccountDetailView.swift | 72 ----------------- .../Core/Views/CoreContentView.swift | 79 ------------------- 2 files changed, 151 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift index e056190e51b..35b763b09fa 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift @@ -16,11 +16,6 @@ struct AccountDetailView: View { @Query private var allMasternodes: [PersistentMasternode] @State private var errorMessage: String? - @State private var copiedText: String? - @State private var showingPrivateKey: String? - @State private var privateKeyToShow: (hex: String, wif: String)? - @State private var showingPINPrompt = false - @State private var pinInput = "" // MARK: Provider derived-keys state /// The #0..#19 keys derived from a provider account's extended @@ -166,18 +161,6 @@ struct AccountDetailView: View { .sheet(item: $selectedTransaction) { transaction in TransactionDetailView(transaction: transaction) } - .sheet(isPresented: $showingPINPrompt) { - PINPromptView( - pinInput: $pinInput, - isPresented: $showingPINPrompt, - onSubmit: { - Task { - await derivePrivateKeyWithPIN() - pinInput = "" - } - } - ) - } .onAppear { appUIState.showWalletsSyncDetails = false } } @@ -1153,59 +1136,4 @@ struct AccountDetailView: View { } return String(format: "%.8f DASH", dash) } - - private func derivePrivateKeyWithPIN() async { - // TODO(platform-wallet): needs new FFI for WIF derivation via - // PlatformWalletManager. For now, surface a stubbed error. - await MainActor.run { - errorMessage = "Private key derivation is not yet available through the new PlatformWalletManager." - } - } -} - -// MARK: - PIN Prompt View - -struct PINPromptView: View { - @Binding var pinInput: String - @Binding var isPresented: Bool - let onSubmit: () -> Void - - var body: some View { - NavigationView { - VStack(spacing: 20) { - Text("Enter Wallet PIN") - .font(.title2) - .fontWeight(.semibold) - - Text("Your PIN is required to access private keys") - .font(.subheadline) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - - SecureField("PIN", text: $pinInput) - .textFieldStyle(.roundedBorder) - .keyboardType(.numberPad) - .padding(.horizontal) - - HStack(spacing: 20) { - Button("Cancel") { - pinInput = "" - isPresented = false - } - .buttonStyle(.bordered) - - Button("Unlock") { - onSubmit() - isPresented = false - } - .buttonStyle(.borderedProminent) - .disabled(pinInput.isEmpty) - } - - Spacer() - } - .padding() - .navigationBarHidden(true) - } - } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift index c4376359aaf..68a95cbac58 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift @@ -969,84 +969,6 @@ struct CompactSyncRow: View { } } -// MARK: - Sync Progress Row (Legacy) - -struct SyncProgressRow: View { - let title: String - let progress: Double - let detail: String - let icon: String - let trailingValue: String? - let onRestart: () -> Void - var navigationDestination: AnyView? = nil - - // Ensure progress is always between 0 and 1 - private var safeProgress: Double { - min(max(progress, 0.0), 1.0) - } - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - // Make only the label tappable if there's a navigation destination - if let destination = navigationDestination { - NavigationLink(destination: destination) { - HStack(spacing: 6) { - Image(systemName: icon) - .font(.subheadline) - Text(title) - .font(.subheadline) - .fontWeight(.semibold) - } - .foregroundColor(.blue) - } - .buttonStyle(PlainButtonStyle()) - } else { - Label(title, systemImage: icon) - .font(.subheadline) - .foregroundColor(.primary) - } - - Spacer() - - if let trailingValue = trailingValue { - Text(trailingValue) - .font(.caption) - .foregroundColor(.secondary) - } - - Button(action: onRestart) { - Image(systemName: "arrow.clockwise") - .font(.caption) - .foregroundColor(.blue) - } - .buttonStyle(BorderlessButtonStyle()) - } - - VStack(alignment: .leading, spacing: 4) { - ProgressView(value: safeProgress) - .progressViewStyle(LinearProgressViewStyle()) - .tint(progressColor(for: safeProgress)) - - Text(detail) - .font(.caption2) - .foregroundColor(.secondary) - } - } - .padding(.vertical, 4) - } - - private func progressColor(for value: Double) -> Color { - if value >= 1.0 { - return .green - } else if value >= 0.5 { - return .blue - } else { - return .orange - } - } -} - // MARK: - Wallet Row View struct WalletRowView: View { @@ -1377,7 +1299,6 @@ private struct QueryCountBadge: View { struct ProofDetailView: View { let proofData: Data @State private var formattedProof: String = "Decoding..." - @State private var copiedText: String? private var proofHex: String { proofData.map { String(format: "%02x", $0) }.joined() From e8ca0daa1dd0302623f9f21f84b0dedbbd744219 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 9 Sep 2026 13:08:15 +0200 Subject: [PATCH 03/11] refactor(swift-example-app): remove dead files across SDK/, Utils/, AppState and SwiftExampleAppApp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Everything below was grep-verified as unreferenced: - `SDK/IdentityBalanceExample.swift` — a print-based tutorial compiled into the app; `exampleFetchBalances` / `exampleWithSecp256k1` appear only at their own definitions. - `SDK/SDKExtensions.swift` — `typealias Signer = SwiftDashSDK.Signer` with no users (only `KeychainSigner` is referenced). - `Utils/TestKeyGenerator.swift` — byte-identical to `Sources/SwiftDashSDK/Helpers/TestKeyGenerator.swift`. Correction to the audit: it claimed the Utils copy "is the copy that is used" — grep shows NEITHER copy has a reference. The SDK twin goes separately. - `test_account_collection.swift` — a `#!/usr/bin/env swift` script that prints a paragraph describing a past change and "Test completed successfully". - `AppState.runStartupDiagnostics` / `runSimpleDiagnostic` — private and never invoked (146 lines). - `SwiftExampleAppApp.readLocalCorePeers` — private and uncalled; `CoreSpvLauncher.peerOverride` re-implements it. `EnvLoader` also loses its hardcoded developer home directories (`/Users/quantum`, `/Users/samuelw`); the current user's path stays. Co-Authored-By: Claude Opus 5 --- .../SwiftExampleApp/AppState.swift | 146 ------------------ .../SDK/IdentityBalanceExample.swift | 59 ------- .../SwiftExampleApp/SDK/SDKExtensions.swift | 9 -- .../SwiftExampleApp/SwiftExampleAppApp.swift | 10 -- .../SwiftExampleApp/Utils/EnvLoader.swift | 7 +- .../Utils/TestKeyGenerator.swift | 46 ------ .../test_account_collection.swift | 46 ------ 7 files changed, 1 insertion(+), 322 deletions(-) delete mode 100644 packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/IdentityBalanceExample.swift delete mode 100644 packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/SDKExtensions.swift delete mode 100644 packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/TestKeyGenerator.swift delete mode 100644 packages/swift-sdk/SwiftExampleApp/test_account_collection.swift diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift index ceb592db09e..6926f6a1b5e 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift @@ -253,150 +253,4 @@ class AppState: ObservableObject { return nil } } - - // MARK: - Startup Diagnostics - - @MainActor - private func runStartupDiagnostics(sdk: SDK) async { - NSLog("====== PLATFORM QUERY DIAGNOSTICS (STARTUP) ======") - - // Test data based on WASM SDK examples - struct TestData { - static let testIdentityId = "6ZhrNvhzD7Qm1nJhWzvipH9cPRLqBamdnXnKjnrrKA2c" - static let testIdentityId2 = "HqyuZoKnHRdKP88Tz5L37whXHa27RuLRoQHzGgJGvCdU" - static let dpnsContractId = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec" - static let testPublicKeyHash = "b7e904ce25ed97594e72f7af0e66f298031c1754" - static let testNonUniquePublicKeyHash = "518038dc858461bcee90478fd994bba8057b7531" - static let testDocumentType = "domain" - static let testUsername = "dash" - static let testTokenId = "Hqyu8WcRwXCTwbNxdga4CN5gsVEGc67wng4TFzceyLUv" - static let testContractId = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec" - static let testDocumentId = "4EfA9Jrvv3nnCFdSf7fad59851iiTRZ6Wcu6YVJ4iSeF" - } - - // Run a few key queries to test connectivity - let diagnosticQueries: [(name: String, test: @MainActor () async throws -> Any)] = [ - ("Get Platform Status", { - try await sdk.getStatus() - }), - - ("Get Total Credits", { - try await sdk.getTotalCreditsInPlatform() - }), - - ("Get Identity", { - try await sdk.identityGet(identityId: TestData.testIdentityId) - }), - - ("Get DPNS Contract", { - try await sdk.dataContractGet(id: TestData.dpnsContractId) - }), - - ("DPNS Check Availability", { - try await sdk.dpnsCheckAvailability(name: "test-name-\(Int.random(in: 1000...9999))") - }) - ] - - var successCount = 0 - var failureCount = 0 - - for query in diagnosticQueries { - NSLog("\n🔍 Testing: \(query.name)") - - do { - let startTime = Date() - let result = try await query.test() - let duration = Date().timeIntervalSince(startTime) - - successCount += 1 - NSLog("✅ Success (\(String(format: "%.3fs", duration)))") - - // Print a summary of the result - if let dict = result as? [String: Any] { - if let version = dict["version"] as? String { - NSLog(" Platform version: \(version)") - } else if let id = dict["id"] as? String { - NSLog(" ID: \(id)") - } else if let balance = dict["balance"] as? UInt64 { - NSLog(" Balance: \(balance)") - } else { - NSLog(" Result: \(dict.keys.prefix(3).joined(separator: ", "))...") - } - } else if let uint = result as? UInt64 { - NSLog(" Value: \(uint)") - } else if let bool = result as? Bool { - NSLog(" Available: \(bool)") - } - - } catch { - failureCount += 1 - NSLog("❌ Failed: \(error.localizedDescription)") - } - } - - NSLog("\n====== DIAGNOSTIC SUMMARY ======") - NSLog("Total queries: \(diagnosticQueries.count)") - NSLog("Successful: \(successCount)") - NSLog("Failed: \(failureCount)") - NSLog("Success rate: \(String(format: "%.0f%%", Double(successCount) / Double(diagnosticQueries.count) * 100))") - NSLog("================================\n") - } - - @MainActor - private func runSimpleDiagnostic(sdk: SDK) async { - var diagnosticReport = "====== SIMPLE DIAGNOSTIC TEST ======\n" - diagnosticReport += "Date: \(Date())\n\n" - - // Test 1: Get Platform Status - do { - diagnosticReport += "Testing: Get Platform Status...\n" - let status = try await sdk.getStatus() - diagnosticReport += "✅ Platform Status Success\n" - let dict = status - diagnosticReport += " Version: \(dict["version"] ?? "unknown")\n" - diagnosticReport += " Mode: \(dict["mode"] ?? "unknown")\n" - diagnosticReport += " QuorumCount: \(dict["quorumCount"] ?? "unknown")\n" - } catch { - diagnosticReport += "❌ Platform Status Failed: \(error)\n" - } - - diagnosticReport += "\n" - - // Test 2: Get Total Credits - do { - diagnosticReport += "Testing: Get Total Credits...\n" - let credits = try await sdk.getTotalCreditsInPlatform() - diagnosticReport += "✅ Total Credits Success: \(credits)\n" - } catch { - diagnosticReport += "❌ Total Credits Failed: \(error)\n" - } - - diagnosticReport += "\n" - - // Test 3: Check DPNS availability - do { - diagnosticReport += "Testing: DPNS Check Availability...\n" - let name = "test-diagnostic-\(Int.random(in: 1000...9999))" - let available = try await sdk.dpnsCheckAvailability(name: name) - diagnosticReport += "✅ DPNS Check Success: name '\(name)' available = \(available)\n" - } catch { - diagnosticReport += "❌ DPNS Check Failed: \(error)\n" - } - - diagnosticReport += "\n====== DIAGNOSTIC COMPLETE ======\n" - - // Write to documents directory - if let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - let diagnosticPath = documentsPath.appendingPathComponent("diagnostic_report.txt") - do { - try diagnosticReport.write(to: diagnosticPath, atomically: true, encoding: .utf8) - NSLog("Diagnostic report written to: \(diagnosticPath)") - } catch { - NSLog("Failed to write diagnostic report: \(error)") - } - } - - // Also log to console - NSLog(diagnosticReport) - } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/IdentityBalanceExample.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/IdentityBalanceExample.swift deleted file mode 100644 index 40cd91c6037..00000000000 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/IdentityBalanceExample.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation -import SwiftDashSDK - -// Example of using the new Data-based fetchBalances API - -func exampleFetchBalances(sdk: SDK) async throws { - // Example 1: Using Data objects directly (recommended for secp256k1 compatibility) - - // Create identity IDs as Data objects (32 bytes each) - let id1 = Data(hexString: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")! - let id2 = Data(hexString: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210")! - - // Fetch balances using Data objects - let balances = try sdk.identities.fetchBalances(ids: [id1, id2]) - - // Process results - for (idData, balance) in balances { - let idHex = idData.toHexString() - if let balance = balance { - print("Identity \(idHex) has balance: \(balance)") - } else { - print("Identity \(idHex) not found") - } - } - - // Example 2: Using string IDs (convenience method) - - let stringIds = [ - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", - "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" - ] - - let dataIds = stringIds.compactMap { Data(hexString: $0) } - let stringBalances = try sdk.identities.fetchBalances(ids: dataIds) - - for (id, balance) in stringBalances { - if let balance = balance { - print("Identity \(id) has balance: \(balance)") - } else { - print("Identity \(id) not found") - } - } -} - - -// Example with secp256k1 integration -// When using swift-secp256k1, you typically have keys/identifiers as 32-byte arrays -// You can convert them to Data for use with fetchBalances: - -func exampleWithSecp256k1() async throws { - // Assuming you have a secp256k1 public key or identifier - // let secp256k1Bytes: [UInt8] = [...] // 32 bytes from secp256k1 - - // Convert to Data - // let identityData = Data(secp256k1Bytes) - - // Use with fetchBalances - // let balances = try sdk.identities.fetchBalances(ids: [identityData]) -} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/SDKExtensions.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/SDKExtensions.swift deleted file mode 100644 index bc0f9d8ca17..00000000000 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/SDKExtensions.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import SwiftDashSDK - -// Re-export SDK types for backward compatibility. -// -// The `Signer` protocol now lives in SwiftDashSDK. Production -// signing is performed by `KeychainSigner` from SwiftDashSDK; the -// legacy `TestSigner` mock has been removed. -public typealias Signer = SwiftDashSDK.Signer diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift index 9fbfa802dcc..58dae52923f 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift @@ -522,16 +522,6 @@ struct SwiftExampleAppApp: App { } } - // MARK: - Helpers - - /// Read local Core peers from UserDefaults (comma-separated addresses). - private func readLocalCorePeers() -> [String] { - if let csv = UserDefaults.standard.string(forKey: "localCorePeers"), !csv.isEmpty { - return csv.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } - } - return ["127.0.0.1"] - } - /// Materialize a `PlatformWalletManager` for every network that /// has an orphan keychain mnemonic, except the already-active /// one. Used during bootstrap so the orphan-recovery flow has diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/EnvLoader.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/EnvLoader.swift index 4a5ee53e55b..26a7a602b68 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/EnvLoader.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/EnvLoader.swift @@ -86,12 +86,7 @@ struct EnvLoader { #if os(iOS) // On iOS simulator, NSHomeDirectory returns the app's sandbox, not the user's home // We need to use hardcoded paths for common usernames - let username = NSUserName() - let possibleHomeDirs = [ - "/Users/\(username)", - "/Users/quantum", - "/Users/samuelw" - ] + let possibleHomeDirs = ["/Users/\(NSUserName())"] for homeDir in possibleHomeDirs { paths.append(contentsOf: [ diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/TestKeyGenerator.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/TestKeyGenerator.swift deleted file mode 100644 index 3e569e669ef..00000000000 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/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/SwiftExampleApp/test_account_collection.swift b/packages/swift-sdk/SwiftExampleApp/test_account_collection.swift deleted file mode 100644 index 3461cf8e185..00000000000 --- a/packages/swift-sdk/SwiftExampleApp/test_account_collection.swift +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env swift - -import Foundation - -// Test script to verify that the AccountCollection FFI functions work correctly -// This script demonstrates how the WalletManager.getAccounts() method now properly -// accesses the Rust AccountCollection structure through the managed account collection FFI - -print("Account Collection Test") -print("======================") -print() -print("The WalletManager.getAccounts() method has been updated to properly use the") -print("managed account collection FFI functions instead of arbitrary account type indices.") -print() -print("Key changes:") -print("1. Uses managed_wallet_get_account_collection() to get the collection") -print("2. Iterates through actual accounts that exist in the collection:") -print(" - BIP44 accounts via managed_account_collection_get_bip44_indices()") -print(" - BIP32 accounts via managed_account_collection_get_bip32_indices()") -print(" - CoinJoin accounts via managed_account_collection_get_coinjoin_indices()") -print(" - Identity registration via managed_account_collection_get_identity_registration()") -print(" - Identity invitation via managed_account_collection_get_identity_invitation()") -print(" - Identity topup accounts via managed_account_collection_get_identity_topup_indices()") -print(" - Provider accounts (voting keys, owner keys, etc.)") -print() -print("3. For each account, it:") -print(" - Gets balance using managed_core_account_get_balance()") -print(" - Returns account information with proper labels") -print(" - Uses unique indices for UI display") -print() -print("The implementation now matches the actual Rust AccountCollection structure:") -print() -print("pub struct AccountCollection {") -print(" pub standard_bip44_accounts: BTreeMap,") -print(" pub standard_bip32_accounts: BTreeMap,") -print(" pub coinjoin_accounts: BTreeMap,") -print(" pub identity_registration: Option,") -print(" pub identity_topup: BTreeMap,") -print(" pub identity_topup_not_bound: Option,") -print(" pub identity_invitation: Option,") -print(" pub provider_voting_keys: Option,") -print(" pub provider_owner_keys: Option,") -print(" // ... etc") -print("}") -print() -print("Test completed successfully! ✅") From 60bdcb20ef10295a16b41869d2ec854b4872e236 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 9 Sep 2026 13:08:33 +0200 Subject: [PATCH 04/11] refactor(swift-example-app): remove dead diagnostic paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `PublicKeyStorageListView.orphanKeys` and the "Unassigned" section: `scopedKeys` already drops every key with `identity == nil`, so the collection is always empty — its own doc comment said as much — and the section could never render. - `walletDisplayLabel(_:fromPersistent:)`: the parameter was only ever passed `nil`. - `DataManagementView` and the "Manage Local Data" row: three destructive buttons with empty closures (`// Clear identities`) and a "Clear All Data" confirmation whose action is `// Implement clear all data`. Non-functional controls in a QA app mislead testers. - The About section hardcoded "SDK Version 1.0.0" / "App Version 1.0.0" while `exportLogs`, a few dozen lines away, already reads the real version from `Bundle.main`. One row with the real version remains. - `DiagnosticsView`: an `#else NSPasteboard` branch in a file that imports UIKit, in an iOS-only target. `fullBase58` from the same audit entry is KEPT — it has four call sites, making it a redundant alias rather than dead code; collapsing it is a separate task. Co-Authored-By: Claude Opus 5 --- .../Views/DiagnosticsView.swift | 5 -- .../SwiftExampleApp/Views/OptionsView.swift | 84 +------------------ .../Views/StorageModelListViews.swift | 24 +----- .../Views/WalletMemoryExplorerView.swift | 7 +- 4 files changed, 6 insertions(+), 114 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DiagnosticsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DiagnosticsView.swift index c3d98ec70c9..31dbd56c339 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DiagnosticsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DiagnosticsView.swift @@ -584,12 +584,7 @@ struct DiagnosticsView: View { } // Copy to pasteboard - #if os(iOS) UIPasteboard.general.string = report - #else - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(report, forType: .string) - #endif showCopiedAlert = true } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift index fcbe452aaa3..f064b6389c8 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift @@ -7,7 +7,6 @@ struct OptionsView: View { @EnvironmentObject var walletManagerStore: WalletManagerStore @EnvironmentObject var platformBalanceSyncService: PlatformBalanceSyncService @EnvironmentObject var shieldedService: ShieldedService - @State private var showingDataManagement = false @State private var showingAbout = false @State private var isSwitchingNetwork = false @State private var isExportingLogs = false @@ -410,10 +409,6 @@ struct OptionsView: View { Label("Banned Addresses", systemImage: "nosign") } - Button(action: { showingDataManagement = true }) { - Label("Manage Local Data", systemImage: "internaldrive") - } - if let stats = appState.dataStatistics { VStack(alignment: .leading, spacing: 8) { Text("Storage Statistics") @@ -484,7 +479,7 @@ struct OptionsView: View { Label("Queries", systemImage: "magnifyingglass") } - NavigationLink(destination: PlatformStateTransitionsView()) { + NavigationLink(destination: StateTransitionsView()) { Label("State Transitions", systemImage: "arrow.up.arrow.down") } @@ -549,17 +544,10 @@ struct OptionsView: View { } } - HStack { - Text("SDK Version") - Spacer() - Text("1.0.0") - .foregroundColor(.secondary) - } - HStack { Text("App Version") Spacer() - Text("1.0.0") + Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?") .foregroundColor(.secondary) } } @@ -569,10 +557,6 @@ struct OptionsView: View { await loadDataStatistics() loadSDKStatus() } - .sheet(isPresented: $showingDataManagement) { - DataManagementView() - .environmentObject(appState) - } .sheet(isPresented: $showingAbout) { AboutView() } @@ -751,70 +735,6 @@ private struct ExportedLogsArchive: Identifiable { var id: URL { url } } -struct DataManagementView: View { - @EnvironmentObject var appState: AppState - @Environment(\.dismiss) var dismiss - @State private var showingClearConfirmation = false - - var body: some View { - NavigationStack { - Form { - Section("Clear Data by Type") { - Button(role: .destructive, action: { - // Clear identities - }) { - Label("Clear All Identities", systemImage: "person.crop.circle.badge.xmark") - } - - Button(role: .destructive, action: { - // Clear documents - }) { - Label("Clear All Documents", systemImage: "doc.badge.xmark") - } - - Button(role: .destructive, action: { - // Clear contracts - }) { - Label("Clear All Contracts", systemImage: "doc.plaintext.badge.xmark") - } - } - - Section("Clear All Data") { - Button(role: .destructive, action: { - showingClearConfirmation = true - }) { - Label("Clear All Data", systemImage: "trash") - .foregroundColor(.red) - } - } - - Section { - Text("Warning: Clearing data will remove all locally stored information for the current network. This action cannot be undone.") - .font(.caption) - .foregroundColor(.secondary) - } - } - .navigationTitle("Manage Data") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - dismiss() - } - } - } - .alert("Clear All Data?", isPresented: $showingClearConfirmation) { - Button("Cancel", role: .cancel) { } - Button("Clear", role: .destructive) { - // Implement clear all data - } - } message: { - Text("This will permanently delete all data for the \(appState.currentNetwork.displayName) network. This action cannot be undone.") - } - } - } -} - struct AboutView: View { @Environment(\.dismiss) var dismiss diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageModelListViews.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageModelListViews.swift index 6a735390481..979b58ba5cc 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageModelListViews.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageModelListViews.swift @@ -85,9 +85,7 @@ struct DataContractStorageListView: View { // MARK: - PersistentPublicKey /// Storage-explorer list of every `PersistentPublicKey`, grouped by -/// owning wallet + identity. Keys without a parent identity land in -/// a trailing "Unassigned" section so they stay visible but don't -/// pollute the wallet-scoped sections above. +/// owning wallet + identity. /// /// Grouping pivot: the `PersistentPublicKey.identity` relationship. /// We drive the top-level order from `PersistentIdentity` sorted by @@ -137,15 +135,6 @@ struct PublicKeyStorageListView: View { ForEach(walletGroups, id: \.walletId) { group in walletSection(group) } - - let orphans = orphanKeys - if !orphans.isEmpty { - Section("Unassigned") { - ForEach(orphans) { key in - keyRow(key) - } - } - } } .navigationTitle("Public Keys (\(scoped.count))") .overlay { @@ -216,17 +205,6 @@ struct PublicKeyStorageListView: View { } } - /// Keys whose `identity` relationship is nil — e.g. rows that - /// predate the changeset wiring or belong to identities since - /// deleted. `scopedKeys` already strips them from the - /// per-network view, so this collection is always empty in the - /// current explorer; the section render below short-circuits on - /// `isEmpty`. Kept as a one-liner so a future global - /// orphan-diagnostics surface can reuse it. - private var orphanKeys: [PersistentPublicKey] { - scopedKeys.filter { $0.identity == nil } - } - private func walletLabel(for walletId: Data) -> String? { guard !walletId.isEmpty else { return nil } return hdWallets.first { $0.walletId == walletId }?.label diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/WalletMemoryExplorerView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/WalletMemoryExplorerView.swift index 43cc20a41e3..f6215d954da 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/WalletMemoryExplorerView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/WalletMemoryExplorerView.swift @@ -27,8 +27,7 @@ private func fullBase58(_ id: Identifier) -> String { id.toBase58() } -private func walletDisplayLabel(_ walletId: Data, fromPersistent name: String?) -> String { - if let name, !name.isEmpty { return name } +private func walletDisplayLabel(_ walletId: Data) -> String { let hex = walletId.prefix(4).map { String(format: "%02x", $0) }.joined() return hex.isEmpty ? "Unknown wallet" : "Wallet \(hex)…" } @@ -335,7 +334,7 @@ struct WalletMemoryExplorerView: View { WalletMemoryDetailView( wallet: wallet, walletId: walletId, - walletLabel: walletDisplayLabel(walletId, fromPersistent: nil) + walletLabel: walletDisplayLabel(walletId) ) } label: { walletRow(walletId: walletId, wallet: wallet) @@ -360,7 +359,7 @@ struct WalletMemoryExplorerView: View { ) let bal = try? wallet.balance() VStack(alignment: .leading, spacing: 4) { - Text(walletDisplayLabel(walletId, fromPersistent: nil)) + Text(walletDisplayLabel(walletId)) .font(.headline) HStack(spacing: 4) { Text("\(summary.identitiesCount) identities") From 7b38d44a6e2ec4fd2af972d4456834d6753cdabf Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 9 Sep 2026 13:09:01 +0200 Subject: [PATCH 05/11] refactor(swift-example-app): remove a pass-through view and rename a file to match its contents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PlatformStateTransitionsView`'s body was `StateTransitionsView()` and it had one caller (OptionsView), which now points straight at `StateTransitionsView`. (The NavigationLink edit itself rode along with the previous commit, together with the rest of the OptionsView changes.) `IdentitiesView.swift` opened with "The IdentitiesView that used to live here was a legacy duplicate … Only IdentityRow stays" — the file is now `IdentityRow.swift`, and the comment describing that past move is gone. Co-Authored-By: Claude Opus 5 --- .../{IdentitiesView.swift => IdentityRow.swift} | 6 ------ .../Views/PlatformStateTransitionsView.swift | 16 ---------------- 2 files changed, 22 deletions(-) rename packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/{IdentitiesView.swift => IdentityRow.swift} (96%) delete mode 100644 packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/PlatformStateTransitionsView.swift diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityRow.swift similarity index 96% rename from packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift rename to packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityRow.swift index 461e2f70446..0198a628fdc 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityRow.swift @@ -2,12 +2,6 @@ import SwiftUI import SwiftData import SwiftDashSDK -// The `IdentitiesView` that used to live here was a legacy -// duplicate of `IdentitiesContentView`. Nothing mounts it; the -// Identities tab renders `IdentitiesContentView` directly. -// Only `IdentityRow` stays — it's the row cell used by -// `IdentitiesContentView`. - /// One row in an identities list. Navigates to `IdentityDetailView` /// on tap. Takes a live `PersistentIdentity` so balance / DPNS name /// edits propagate reactively via `@Query` upstream without any diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/PlatformStateTransitionsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/PlatformStateTransitionsView.swift deleted file mode 100644 index 10a6c38daf8..00000000000 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/PlatformStateTransitionsView.swift +++ /dev/null @@ -1,16 +0,0 @@ -import SwiftUI - -struct PlatformStateTransitionsView: View { - var body: some View { - StateTransitionsView() - } -} - -struct PlatformStateTransitionsView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - PlatformStateTransitionsView() - .environmentObject(AppState()) - } - } -} From e80eb212556decfad432a8745d0a9b9bc6783c97 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 9 Sep 2026 13:09:13 +0200 Subject: [PATCH 06/11] refactor(swift-example-app): remove the "View Private Key" button wired to an empty stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Button(action: viewPrivateKey)` called a private method with an empty body — two comments and nothing else. A visible control that does nothing. While here, `hasPrivateKey` stops printing to the console on every evaluation of that computed property. The mismatch between `KeyDetailView.hasPrivateKey` (legacy Keychain scheme only) and `KeysListView.hasPrivateKey` (both schemes) is KEPT — that is a behaviour bug rather than dead code, and fixing it needs a shared helper on the SDK side. Reported separately. Co-Authored-By: Claude Opus 5 --- .../SwiftExampleApp/Views/KeyDetailView.swift | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeyDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeyDetailView.swift index 48ee5b54179..cd0a801cca2 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeyDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeyDetailView.swift @@ -19,9 +19,10 @@ struct KeyDetailView: View { @EnvironmentObject var walletManager: PlatformWalletManager var hasPrivateKey: Bool { - let result = KeychainManager.shared.hasPrivateKey(identityId: identity.identityId, keyIndex: Int32(publicKey.id)) - print("🔑 KeyDetailView: hasPrivateKey for key \(publicKey.id) = \(result)") - return result + KeychainManager.shared.hasPrivateKey( + identityId: identity.identityId, + keyIndex: Int32(publicKey.id) + ) } /// Pre-flight gate for disabling this key. Evaluated against the @@ -85,10 +86,6 @@ struct KeyDetailView: View { Text("Private key is stored securely") } - Button(action: viewPrivateKey) { - Label("View Private Key", systemImage: "eye.fill") - } - Button(action: { showForgetKeyAlert = true }) { Label("Forget Private Key", systemImage: "trash") } @@ -220,11 +217,6 @@ struct KeyDetailView: View { } } - private func viewPrivateKey() { - // This will trigger the sheet presentation through the parent view - // For now, we could show an alert or navigate to a secure view - } - private func validateAndStorePrivateKey() { isValidating = true validationError = nil From 85d43cae6b2531c91979e54db8ebceb8c92dc5f4 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 9 Sep 2026 13:09:26 +0200 Subject: [PATCH 07/11] refactor(swift-example-app): remove the dead contestInfo parameter from ContestDetailView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `let contestInfo: [String: Any]` was documented as "Kept for call-site compatibility but unused — the view reads everything off `voteState`". A parameter that exists only to avoid editing one call site is the definition of a vestigial shim. Its sole caller (IdentityDetailView) built it from `contestedDpnsInfo[name] as? [String: Any] ?? [:]`. The duplicated DPNS constants from the same audit entry (the contract literal appears 11 times across the app) are KEPT — collapsing them onto the SDK's `DPNSVotePoll` is constant substitution, not dead-code removal. Co-Authored-By: Claude Opus 5 --- .../SwiftExampleApp/Views/ContestDetailView.swift | 8 +------- .../SwiftExampleApp/Views/IdentityDetailView.swift | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContestDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContestDetailView.swift index 0a674a180da..0ade574fe7c 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContestDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContestDetailView.swift @@ -13,16 +13,10 @@ import SwiftDashSDK /// `"ResourceVote { vote_choice: TowardsIdentity(...), strength: 1 }"`) /// and lets the view render straight off strongly-typed fields. /// -/// The `contestInfo` dict is still accepted on the init to preserve -/// the navigation-link callers, but the view no longer reads from -/// it — fresh state comes from the wallet path on appear + on +/// Fresh state comes from the wallet path on appear + on /// pull-to-refresh. struct ContestDetailView: View { let contestName: String - /// Legacy `[String: Any]` payload from callers that predate the - /// wallet-path migration. Kept for call-site compatibility but - /// unused — the view reads everything off `voteState`. - let contestInfo: [String: Any] /// Identity viewing the contest. Used both for "You" badging on /// the viewer's own contender row and for the wallet-path /// lookup filter. diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift index edc3ed10dbf..4d9a0d6a33a 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift @@ -258,7 +258,6 @@ struct IdentityDetailView: View { ForEach(contestedDpnsNames, id: \.self) { name in NavigationLink(destination: ContestDetailView( contestName: name, - contestInfo: contestedDpnsInfo[name] as? [String: Any] ?? [:], currentIdentityId: identity.identityIdBase58 ).environmentObject(appState)) { HStack { From 73a3ade02770eaab7b6845e64bfbf1f86a84e8ef Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 9 Sep 2026 13:09:38 +0200 Subject: [PATCH 08/11] refactor(swift-example-app): remove iOS<17 onChange shims and a stdlib-shadowing extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DocumentIdChangeHandler` and `UsernameChangeHandler` branched on `#available(iOS 17.0, *)` while project.pbxproj sets IPHONEOS_DEPLOYMENT_TARGET = 18.5 and every other view calls the two-argument `onChange` directly. Both call sites move to `.onChange(of:)` inline; the dead modifiers go. `extension Character { var isHexDigit }` shadowed the standard library property of the same name — it could drift from stdlib semantics while adding nothing. The `print("DEBUG: …")` storms from this entry are KEPT — silencing logs needs its own change (a single `Log.debug` behind `#if DEBUG`) and is not dead-code removal. Co-Authored-By: Claude Opus 5 --- .../Views/DocumentWithPriceView.swift | 26 +++---------------- .../Views/RegisterNameView.swift | 16 ++---------- 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentWithPriceView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentWithPriceView.swift index 3c71427663a..235395afdfb 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentWithPriceView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentWithPriceView.swift @@ -23,9 +23,9 @@ struct DocumentWithPriceView: View { HStack { TextField("Enter document ID", text: $documentId) .textFieldStyle(RoundedBorderTextFieldStyle()) - .modifier(DocumentIdChangeHandler(documentId: $documentId) { - handleDocumentIdChange($0) - }) + .onChange(of: documentId) { _, newValue in + handleDocumentIdChange(newValue) + } if isLoading { ProgressView() @@ -345,23 +345,3 @@ struct DocumentWithPriceView: View { } } } - -// Cross-version onChange helper for documentId -private struct DocumentIdChangeHandler: ViewModifier { - @Binding var documentId: String - let onChange: (String) -> Void - func body(content: Content) -> some View { - if #available(iOS 17.0, *) { - content.onChange(of: documentId) { _, newValue in onChange(newValue) } - } else { - content.onChange(of: documentId) { newValue in onChange(newValue) } - } - } -} - -// Extension to check if character is hex digit -extension Character { - var isHexDigit: Bool { - return "0123456789abcdefABCDEF".contains(self) - } -} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/RegisterNameView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/RegisterNameView.swift index 5ebe7a7d2d1..af9ee277e44 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/RegisterNameView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/RegisterNameView.swift @@ -164,7 +164,7 @@ struct RegisterNameView: View { .textContentType(.username) .autocapitalization(.none) .autocorrectionDisabled(true) - .modifier(UsernameChangeHandler(username: $username) { + .onChange(of: username) { _, _ in // Cancel any existing timer checkTimer?.invalidate() @@ -186,7 +186,7 @@ struct RegisterNameView: View { } } } - }) + } if !normalizedUsername.isEmpty { VStack(alignment: .leading, spacing: 4) { @@ -471,18 +471,6 @@ struct RegisterNameView: View { } -private struct UsernameChangeHandler: ViewModifier { - @Binding var username: String - let onChange: () -> Void - func body(content: Content) -> some View { - if #available(iOS 17.0, *) { - content.onChange(of: username) { _, _ in onChange() } - } else { - content.onChange(of: username) { _ in onChange() } - } - } -} - // Preview removed — constructing a sample `PersistentIdentity` // requires a mock `ModelContainer`; not worth the scaffolding for // this dev example app. Restore via `#Preview { … }` with a From e58f20d9105c66a90b7527e3117d5d73c8428646 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 9 Sep 2026 13:09:56 +0200 Subject: [PATCH 09/11] refactor(swift-example-app): remove dead alias/note/hidden API from DashPayContactMetaStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the `contactInfo` migration, alias, note and the hidden flag live on `PersistentDashpayContactRequest` rows written through `setDashPayContactInfo`. The UserDefaults store was left with a dead half: `setAlias`, `setNote`, `setHidden`, `note` and `isHidden` have no call site at all. The one read, `contactMeta.alias(...)` in `ContactRequestsView.displayName`, could never return a value — nothing wrote that key — while looking like a real display-name precedence rule. It now passes `alias: nil`. The store is reduced to `dpnsHint`/`setDpnsHint` (the DPNS hint captured when a contact is added, which has no `contactInfo` counterpart), and its header describes what it actually does instead of announcing a migration that already happened. `DashPayContact.note` / `.isHidden` were only ever set to their defaults by the single constructor and never read — they go along with the hand-written `init` (the memberwise one suffices). The `ContactLocalFieldEditor` doc claimed it writes to `DashPayContactMetaStore`; in fact its `onSave` calls `saveContactInfo`. Co-Authored-By: Claude Opus 5 --- .../Views/DashPay/ContactDetailView.swift | 8 ++-- .../Views/DashPay/ContactRequestsView.swift | 6 +-- .../Views/DashPay/DashPayContactMeta.swift | 44 +++---------------- .../DashPay/SendDashPayPaymentSheet.swift | 18 -------- 4 files changed, 11 insertions(+), 65 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ContactDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ContactDetailView.swift index 7261195885f..752f8b4c77c 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ContactDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ContactDetailView.swift @@ -514,10 +514,10 @@ struct PaymentHistoryRow: View { // MARK: - Local field editor -/// Tiny Form-based editor sheet for the device-local alias / note -/// fields — same shape as `EditAliasView` but writing to the -/// `DashPayContactMetaStore` instead of a SwiftData row. Saving an -/// empty value clears the field. +/// Tiny Form-based editor sheet for the contact's alias / note +/// fields — same shape as `EditAliasView`. Persisting is the caller's +/// job: `onSave` routes to `saveContactInfo`. Saving an empty value +/// clears the field. struct ContactLocalFieldEditor: View { let title: String let prompt: String diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ContactRequestsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ContactRequestsView.swift index 0ad941084cb..1beb3ba9ebd 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ContactRequestsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ContactRequestsView.swift @@ -260,11 +260,7 @@ struct ContactRequestsView: View { _ = contactMeta.version return dashPayContactDisplayName( contactId: contactId, - alias: contactMeta.alias( - network: identity.network, - owner: identity.identityId, - contact: contactId - ), + alias: nil, profileDisplayName: cachedProfile(contactId)?.displayName, dpnsLabel: contactMeta.dpnsHint( network: identity.network, diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/DashPayContactMeta.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/DashPayContactMeta.swift index 18a8f6738db..8e188d42d34 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/DashPayContactMeta.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/DashPayContactMeta.swift @@ -2,17 +2,16 @@ import Foundation import SwiftUI import SwiftDashSDK -/// Device-local, per-contact metadata for the DashPay tab: alias, -/// note, hidden flag, and a DPNS-label hint captured at add time. +/// Device-local DPNS-label hint captured when a contact is added by +/// username search. /// -/// These are scoped to "This device only" — a later milestone replaces -/// this store with `contactInfo` documents synced via Platform. Until -/// then UserDefaults is the honest backing: no sync semantics exist, so -/// none are implied. +/// Alias, note and hidden now live on the `PersistentDashpayContactRequest` +/// rows written through `setDashPayContactInfo`; this store keeps only the +/// add-time hint, which has no `contactInfo` counterpart. /// /// Keys are scoped by `(network, owner identity, contact identity)` /// so two owner identities (or two networks) never share a contact's -/// alias. The published `version` counter makes SwiftUI views that +/// hint. The published `version` counter makes SwiftUI views that /// read through this store re-render after a write — UserDefaults /// alone doesn't participate in SwiftUI invalidation for computed /// reads. @@ -23,37 +22,6 @@ final class DashPayContactMetaStore: ObservableObject { private let defaults = UserDefaults.standard - // MARK: - Alias (local display-name override) - - func alias(network: Network, owner: Data, contact: Data) -> String? { - nonEmpty(defaults.string(forKey: key("alias", network, owner, contact))) - } - - func setAlias(_ alias: String?, network: Network, owner: Data, contact: Data) { - write(nonEmpty(alias), forKey: key("alias", network, owner, contact)) - } - - // MARK: - Note - - func note(network: Network, owner: Data, contact: Data) -> String? { - nonEmpty(defaults.string(forKey: key("note", network, owner, contact))) - } - - func setNote(_ note: String?, network: Network, owner: Data, contact: Data) { - write(nonEmpty(note), forKey: key("note", network, owner, contact)) - } - - // MARK: - Hidden - - func isHidden(network: Network, owner: Data, contact: Data) -> Bool { - defaults.bool(forKey: key("hidden", network, owner, contact)) - } - - func setHidden(_ hidden: Bool, network: Network, owner: Data, contact: Data) { - defaults.set(hidden, forKey: key("hidden", network, owner, contact)) - version += 1 - } - // MARK: - DPNS hint /// DPNS label observed when the contact was added via username diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/SendDashPayPaymentSheet.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/SendDashPayPaymentSheet.swift index 8b2b56f03b5..bfdbb31a897 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/SendDashPayPaymentSheet.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/SendDashPayPaymentSheet.swift @@ -11,24 +11,6 @@ struct DashPayContact: Identifiable { let displayName: String let identityId: Data let dpnsName: String? - let note: String? - let isHidden: Bool - - init( - id: Data, - displayName: String, - identityId: Data, - dpnsName: String? = nil, - note: String? = nil, - isHidden: Bool = false - ) { - self.id = id - self.displayName = displayName - self.identityId = identityId - self.dpnsName = dpnsName - self.note = note - self.isHidden = isHidden - } } // MARK: - Send payment sheet From 29421fe5c8139153e4e9e6cead6221c494df0585 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 9 Sep 2026 13:10:11 +0200 Subject: [PATCH 10/11] refactor(swift-example-app): remove the unused seed phrase field and stubs from the generic transition builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `identityCreate` definition required a `seedPhrase` field (`required: true`) plus a "Generate New Seed" button. The handler `generateTestSeedPhrase()` returned the literal "test seed phrase for development only do not use in production ever please", and `executeIdentityCreate` never reads `formInputs["seedPhrase"]` — it calls `sdk.identityCreate()`. A QA tester had to type a fake seed to enable a button whose result ignores it. Also from the same action switch: - `case "loadExistingDocument"` and `case "fetchContestedResources"` — empty TODO arms, - `fetchDocumentSchema` — a TODO writing a canned `{ "message": … }` template into `documentFields`, which `DocumentFieldsView` immediately overwrites. `TransitionCategoryView` stops advertising `masternodeVote`, since `executeStateTransition` routes it to `default: notImplemented`. Co-Authored-By: Claude Opus 5 --- .../Models/StateTransitionDefinitions.swift | 15 ------ .../Views/TransitionCategoryView.swift | 6 +-- .../Views/TransitionDetailView.swift | 48 ------------------- 3 files changed, 3 insertions(+), 66 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Models/StateTransitionDefinitions.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Models/StateTransitionDefinitions.swift index 9e94eea237d..f4f308698c6 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Models/StateTransitionDefinitions.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Models/StateTransitionDefinitions.swift @@ -10,21 +10,6 @@ public struct TransitionDefinitions { label: "Identity Create", description: "Create a new identity with initial credits", inputs: [ - TransitionInput( - name: "seedPhrase", - type: "textarea", - label: "Seed Phrase", - required: true, - placeholder: "Enter seed phrase (12-24 words) or click Generate", - help: "The wallet seed phrase that will be used to derive identity keys" - ), - TransitionInput( - name: "generateSeedButton", - type: "button", - label: "Generate New Seed", - required: false, - action: "generateTestSeed" - ), TransitionInput( name: "identityIndex", type: "number", diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionCategoryView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionCategoryView.swift index 925c8bbba0f..2cc6bb44d45 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionCategoryView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionCategoryView.swift @@ -45,9 +45,9 @@ struct TransitionCategoryView: View { ("tokenSetPrice", "Set Token Price", "Set or update token pricing") ] case .voting: - return [ - ("masternodeVote", "Cast Vote", "Vote on a governance proposal") - ] + // No voting transition is wired yet — executeStateTransition + // routes masternodeVote to `notImplemented`. + return [] } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionDetailView.swift index 3a63699c249..3dd4c79a00f 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionDetailView.swift @@ -443,34 +443,9 @@ struct TransitionDetailView: View { let docType = String(action.dropFirst("documentTypeSelected:".count)) selectedDocumentType = docType formInputs["documentType"] = docType - // Fetch schema for the selected document type - fetchDocumentSchema(contractId: selectedContractId, documentType: docType) - } else { - switch action { - case "generateTestSeed": - // Generate a test seed phrase - formInputs["seedPhrase"] = generateTestSeedPhrase() - case "fetchDocumentSchema": - if !selectedContractId.isEmpty && !selectedDocumentType.isEmpty { - fetchDocumentSchema(contractId: selectedContractId, documentType: selectedDocumentType) - } - case "loadExistingDocument": - // TODO: Load existing document - break - case "fetchContestedResources": - // TODO: Fetch contested resources - break - default: - break - } } } - private func generateTestSeedPhrase() -> String { - // This is a placeholder - in production, use proper BIP39 generation - return "test seed phrase for development only do not use in production ever please" - } - private func getTransitionDefinition(_ key: String) -> TransitionDefinition? { return TransitionDefinitions.all[key] } @@ -2398,29 +2373,6 @@ struct TransitionDetailView: View { return input } - private func fetchDocumentSchema(contractId: String, documentType: String) { - // TODO: Implement fetching schema and generating dynamic form - // For now, provide a template based on common patterns - var schemaTemplate = "{\n" - - // Common document type templates - switch documentType.lowercased() { - case "note", "message": - schemaTemplate += " \"message\": \"Enter your message here\"\n" - case "profile", "user": - schemaTemplate += " \"displayName\": \"John Doe\",\n" - schemaTemplate += " \"bio\": \"About me...\"\n" - case "post": - schemaTemplate += " \"title\": \"Post title\",\n" - schemaTemplate += " \"content\": \"Post content...\"\n" - default: - schemaTemplate += " // Add document fields here\n" - } - - schemaTemplate += "}" - formInputs["documentFields"] = schemaTemplate - } - private func normalizeIdentityId(_ identityId: String) -> String { // Remove any prefix let cleanId = identityId From 631efb45ddc1d80bbf0de629155c9974a254fb68 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Thu, 10 Sep 2026 14:57:09 +0200 Subject: [PATCH 11/11] fix(swift-example-app): keep hex validation ASCII-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dropping `extension Character { var isHexDigit }` handed its two call sites the stdlib property, which is broader: it also accepts the fullwidth forms (U+FF10 `0`, U+FF21 `A`, …) that the removed extension rejected. Both sites gate a machine format — a 42-character hex address and a 64-character contract id — where those code points are never valid. Spell the intent out with `isHexDigit && isASCII`, which is exactly equivalent to the removed extension (verified over U+0000…U+2FFFF). `isLikelyContractIdBytes` was already covered by the `Data(hexString:)` decode that follows, so only the address-format label actually regressed; the contract-id site is tightened for the same reason and carries the note. Co-Authored-By: Claude Opus 5 --- .../ViewModels/GetAddressInfoViewModel.swift | 2 +- .../SwiftExampleApp/Views/ContractsTabView.swift | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ViewModels/GetAddressInfoViewModel.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ViewModels/GetAddressInfoViewModel.swift index 2eeedca1523..904c7742006 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ViewModels/GetAddressInfoViewModel.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ViewModels/GetAddressInfoViewModel.swift @@ -24,7 +24,7 @@ final class GetAddressInfoViewModel: BaseViewModel { } else { return ("xmark.circle.fill", .red, "Invalid bech32m address") } - } else if trimmed.count == 42 && trimmed.allSatisfy({ $0.isHexDigit }) { + } else if trimmed.count == 42 && trimmed.allSatisfy({ $0.isHexDigit && $0.isASCII }) { return ("checkmark.circle.fill", .green, "Hex format (42 characters)") } else if !trimmed.isEmpty { return ("questionmark.circle", .orange, "Unknown format") diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContractsTabView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContractsTabView.swift index fcf416d5b5c..e864cf0cc96 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContractsTabView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContractsTabView.swift @@ -883,9 +883,11 @@ struct ContractsTabView: View { let stripped = raw.trimmingCharacters(in: .whitespacesAndNewlines) if stripped.isEmpty { return nil } - // Hex: must be exactly 64 chars and all hex digits. + // Hex: must be exactly 64 chars and all ASCII hex digits. + // `Character.isHexDigit` alone also accepts the fullwidth + // forms (U+FF10…), which `Data(hexString:)` cannot decode. if stripped.count == 64, - stripped.allSatisfy({ $0.isHexDigit }), + stripped.allSatisfy({ $0.isHexDigit && $0.isASCII }), let data = Data(hexString: stripped), data.count == 32 { return data