From 09c8aef016651602caf76a1a9e3623c286b5fe27 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 20 Mar 2026 11:25:03 +0300 Subject: [PATCH 01/27] feat(swift-sdk): add ZK shielded sync status UI with periodic note and nullifier sync - Create ZKSyncService with periodic sync loop (30s interval) - Display shielded balance, orchard address, note/nullifier stats in CoreContentView - Persist balance and address across app launches via UserDefaults - Wire into UnifiedAppState lifecycle and pull-to-refresh Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Services/ZKSyncService.swift | 164 ++++++++++++++++++ .../Core/Views/CoreContentView.swift | 140 +++++++++++++++ .../Core/Views/WalletsContentView.swift | 1 + .../SwiftExampleApp/SwiftExampleAppApp.swift | 1 + .../SwiftExampleApp/UnifiedAppState.swift | 51 ++++++ 5 files changed, 357 insertions(+) create mode 100644 packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/ZKSyncService.swift diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/ZKSyncService.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/ZKSyncService.swift new file mode 100644 index 0000000000..fe924f8409 --- /dev/null +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/ZKSyncService.swift @@ -0,0 +1,164 @@ +// ZKSyncService.swift +// SwiftExampleApp +// +// App-level service that performs periodic ZK shielded sync (notes + nullifiers) +// with UI status display. Follows the same pattern as PlatformBalanceSyncService. + +import Foundation +import SwiftUI +import SwiftDashSDK + +/// Observable service managing periodic ZK shielded pool sync. +/// +/// Syncs every 30 seconds while the app is active, or on manual pull-to-refresh. +/// Persists `shieldedBalance` and `orchardAddress` in UserDefaults for display across launches. +@MainActor +class ZKSyncService: ObservableObject { + // MARK: - Published State + + /// Whether a sync is currently in progress. + @Published var isSyncing: Bool = false + + /// Last successful sync time (local clock). + @Published var lastSyncTime: Date? + + /// Current shielded balance (in credits). + @Published var shieldedBalance: UInt64 = 0 + + /// Orchard display address (Bech32m-encoded). + @Published var orchardAddress: String? + + /// Number of new notes found in the most recent sync. + @Published var notesSynced: Int = 0 + + /// Number of nullifiers spent in the most recent sync. + @Published var nullifiersSpent: Int = 0 + + /// Cumulative notes synced since launch. + @Published var totalNotesSynced: Int = 0 + + /// Cumulative nullifiers spent since launch. + @Published var totalNullifiersSpent: Int = 0 + + /// Total number of successful syncs since launch. + @Published var syncCountSinceLaunch: Int = 0 + + /// Last error message, cleared on successful sync. + @Published var lastError: String? + + // MARK: - Persisted State + + /// Persisted shielded balance (credits). + private var persistedBalance: UInt64 { + get { UInt64(UserDefaults.standard.integer(forKey: "\(keyPrefix)_balance")) } + set { UserDefaults.standard.set(Int(newValue), forKey: "\(keyPrefix)_balance") } + } + + /// Persisted orchard address string. + private var persistedOrchardAddress: String? { + get { UserDefaults.standard.string(forKey: "\(keyPrefix)_orchardAddress") } + set { UserDefaults.standard.set(newValue, forKey: "\(keyPrefix)_orchardAddress") } + } + + /// UserDefaults key prefix scoped to network. + private var keyPrefix: String { + "zkSync_\(networkName)" + } + + private var networkName: String = "testnet" + + // MARK: - Lifecycle + + /// Initialize for a network. Restores persisted balance and address. + /// The actual periodic loop is managed by UnifiedAppState. + func startPeriodicSync(network: AppNetwork) { + networkName = network.rawValue + + // Restore persisted state from previous session + let savedBalance = persistedBalance + if savedBalance > 0 { + shieldedBalance = savedBalance + } + + let savedAddress = persistedOrchardAddress + if let addr = savedAddress, !addr.isEmpty { + orchardAddress = addr + } + } + + /// Perform a single ZK shielded sync (notes then nullifiers). + /// + /// - Parameters: + /// - sdk: The initialized SDK instance. + /// - shieldedService: The shielded service with an initialized pool client. + func performSync(sdk: SDK, shieldedService: ShieldedService) async { + guard !isSyncing else { return } + guard let poolClient = shieldedService.poolClient else { return } + + isSyncing = true + lastError = nil + + do { + // Step 1: Sync notes + let notesResult = try await poolClient.syncNotes(sdk: sdk) + let newNotes = notesResult.newNotes + + // Step 2: Sync nullifiers + let nullifiersResult = try await poolClient.syncNullifiers(sdk: sdk) + let spentCount = nullifiersResult.spentCount + let finalBalance = nullifiersResult.balance + + // Update per-sync stats + notesSynced = newNotes + nullifiersSpent = spentCount + + // Update cumulative stats + totalNotesSynced += newNotes + totalNullifiersSpent += spentCount + + // Update balance and address + shieldedBalance = finalBalance + orchardAddress = shieldedService.orchardDisplayAddress + + // Persist balance and address + persistedBalance = finalBalance + persistedOrchardAddress = shieldedService.orchardDisplayAddress + + // Update sync metadata + lastSyncTime = Date() + syncCountSinceLaunch += 1 + + SDKLogger.log( + "ZK sync complete: \(newNotes) notes, \(spentCount) spent, balance: \(finalBalance)", + minimumLevel: .medium + ) + + } catch { + lastError = error.localizedDescription + SDKLogger.log( + "ZK sync error: \(error.localizedDescription)", + minimumLevel: .medium + ) + } + + isSyncing = false + } + + /// Reset all state (e.g. on wallet deletion or network switch). + func reset() { + isSyncing = false + lastSyncTime = nil + shieldedBalance = 0 + orchardAddress = nil + notesSynced = 0 + nullifiersSpent = 0 + totalNotesSynced = 0 + totalNullifiersSpent = 0 + syncCountSinceLaunch = 0 + lastError = nil + + // Clear persisted state + persistedBalance = 0 + persistedOrchardAddress = nil + } +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift index 77dab8ee47..13826bac0b 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift @@ -6,6 +6,7 @@ struct CoreContentView: View { @EnvironmentObject var walletService: WalletService @EnvironmentObject var unifiedAppState: UnifiedAppState @EnvironmentObject var platformBalanceSyncService: PlatformBalanceSyncService + @EnvironmentObject var zkSyncService: ZKSyncService @State private var showProofDetail = false // Progress values come from WalletService (kept in sync with SPV callbacks) @@ -299,6 +300,145 @@ var body: some View { Text("Platform Sync Status") } + // Section 3: ZK Shielded Sync Status + Section { + VStack(spacing: 8) { + // Sync state row + HStack { + if zkSyncService.isSyncing { + ProgressView() + .scaleEffect(0.7) + Text("Syncing...") + .font(.subheadline) + .foregroundColor(.secondary) + } else if let lastSync = zkSyncService.lastSyncTime { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + .font(.caption) + Text("Last sync: \(lastSync, style: .relative)") + .font(.caption) + .foregroundColor(.secondary) + } else { + Image(systemName: "circle.dashed") + .foregroundColor(.secondary) + .font(.caption) + Text("Not synced yet") + .font(.subheadline) + .foregroundColor(.secondary) + } + Spacer() + } + + // Shielded balance + HStack { + Text("Shielded Balance") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + if zkSyncService.shieldedBalance > 0 { + Text(formatCredits(zkSyncService.shieldedBalance)) + .font(.subheadline) + .fontWeight(.medium) + } else { + Text("0") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + + // Orchard address (truncated) + if let address = zkSyncService.orchardAddress { + HStack { + Text("Orchard Address") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text(String(address.prefix(12)) + "..." + String(address.suffix(6))) + .foregroundColor(.secondary) + .font(.system(.caption, design: .monospaced)) + } + } + + // Last sync stats + HStack { + Text("Last Sync") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text("\(zkSyncService.notesSynced) notes, \(zkSyncService.nullifiersSpent) spent") + .font(.caption) + .foregroundColor(.secondary) + } + + // Cumulative totals + HStack { + Text("Total Synced") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text("\(zkSyncService.totalNotesSynced) notes, \(zkSyncService.totalNullifiersSpent) spent") + .font(.caption) + .foregroundColor(.secondary) + } + + // Sync count + HStack { + Text("Sync Count") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text("\(zkSyncService.syncCountSinceLaunch)") + .font(.caption) + .foregroundColor(.secondary) + } + + // Error display + if let error = zkSyncService.lastError { + Text(error) + .font(.caption) + .foregroundColor(.red) + .lineLimit(2) + } + + // Action buttons + HStack { + Spacer() + + Button { + Task { + await unifiedAppState.performZKSync() + } + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.clockwise") + Text("Sync Now") + } + .font(.caption) + .fontWeight(.medium) + } + .buttonStyle(.borderedProminent) + .tint(.blue) + .controlSize(.mini) + .disabled(zkSyncService.isSyncing) + + Button { + zkSyncService.reset() + } label: { + Text("Clear") + .font(.caption) + .fontWeight(.medium) + } + .buttonStyle(.borderedProminent) + .tint(.red) + .controlSize(.mini) + .disabled(zkSyncService.isSyncing) + } + } + .padding(.vertical, 4) + } header: { + Text("ZK Shielded Sync") + } + } .navigationTitle("Sync Status") .onAppear { diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletsContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletsContentView.swift index 4a4199c097..315283e80a 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletsContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletsContentView.swift @@ -126,6 +126,7 @@ struct WalletsContentView: View { } .refreshable { await unifiedAppState.performPlatformBalanceSync() + await unifiedAppState.performZKSync() } } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift index f53c7b7401..b35e025168 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SwiftExampleAppApp.swift @@ -50,6 +50,7 @@ struct SwiftExampleAppApp: App { .environmentObject(unifiedState.unifiedState) .environmentObject(unifiedState.shieldedService) .environmentObject(unifiedState.platformBalanceSyncService) + .environmentObject(unifiedState.zkSyncService) .environment(\.modelContext, unifiedState.modelContainer.mainContext) .task { SDKLogger.log("πŸš€ SwiftExampleApp: Starting initialization...", minimumLevel: .medium) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift index f205bdef6c..3b10c146cc 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift @@ -32,6 +32,9 @@ class UnifiedAppState: ObservableObject { // Platform address balance sync service (BLAST sync) let platformBalanceSyncService = PlatformBalanceSyncService() + // ZK shielded pool sync service + let zkSyncService = ZKSyncService() + // State from Platform let platformState: AppState @@ -47,6 +50,9 @@ class UnifiedAppState: ObservableObject { // Task for the periodic sync loop private var syncLoopTask: Task? + // Task for the periodic ZK sync loop + private var zkSyncLoopTask: Task? + // Computed property for easy SDK access var sdk: SDK? { platformState.sdk @@ -93,6 +99,9 @@ class UnifiedAppState: ObservableObject { // Start periodic BLAST address sync startPlatformBalanceSync() + // Start periodic ZK shielded sync + startZKSync() + isInitialized = true } @@ -106,6 +115,9 @@ class UnifiedAppState: ObservableObject { syncLoopTask?.cancel() syncLoopTask = nil platformBalanceSyncService.reset() + zkSyncLoopTask?.cancel() + zkSyncLoopTask = nil + zkSyncService.reset() // Reset platform state platformState.sdk = nil @@ -129,6 +141,9 @@ class UnifiedAppState: ObservableObject { // Restart BLAST sync for the new network startPlatformBalanceSync() + // Restart ZK sync for the new network + startZKSync() + // The platform state handles its own network switching in AppState.switchNetwork } @@ -189,6 +204,42 @@ class UnifiedAppState: ObservableObject { } } + /// Start periodic ZK shielded sync (every 30 seconds). + func startZKSync() { + // Cancel any previous sync loop + zkSyncLoopTask?.cancel() + + let network = platformState.currentNetwork + zkSyncService.startPeriodicSync(network: network) + + // Run a repeating async loop + zkSyncLoopTask = Task { [weak self] in + // Initial delay to allow SDK and shielded service to initialize + try? await Task.sleep(for: .seconds(5)) + await self?.performZKSync() + + // Repeat every 30 seconds + while !Task.isCancelled { + do { + try await Task.sleep(for: .seconds(30)) + } catch { + break // Task was cancelled + } + await self?.performZKSync() + } + } + } + + /// Perform a single ZK shielded sync. Skips silently if no SDK or pool client. + func performZKSync() async { + guard let sdk = platformState.sdk else { return } + + // Skip silently if shielded pool client is not initialized + guard shieldedService.poolClient != nil else { return } + + await zkSyncService.performSync(sdk: sdk, shieldedService: shieldedService) + } + /// Initialize the shielded service using the first wallet's seed. /// Call after wallet seed becomes available or on network switch. func initializeShieldedService() { From 5ebd4bca1606bc8b0c9027223e993f918f1430f9 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 20 Mar 2026 11:53:25 +0300 Subject: [PATCH 02/27] feat(swift-sdk): add collapsible sync status sections and remove animations Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Views/CoreContentView.swift | 366 ++++++++++-------- 1 file changed, 196 insertions(+), 170 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift index 13826bac0b..623995ab12 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift @@ -8,6 +8,8 @@ struct CoreContentView: View { @EnvironmentObject var platformBalanceSyncService: PlatformBalanceSyncService @EnvironmentObject var zkSyncService: ZKSyncService @State private var showProofDetail = false + @State private var showPlatformDetails = false + @State private var showZKDetails = false // Progress values come from WalletService (kept in sync with SPV callbacks) // Display helpers @@ -140,6 +142,15 @@ var body: some View { .foregroundColor(.secondary) } Spacer() + // Expand/collapse chevron + Button { + showPlatformDetails.toggle() + } label: { + Image(systemName: showPlatformDetails ? "chevron.up" : "chevron.down") + .font(.caption) + .foregroundColor(.secondary) + } + .buttonStyle(.plain) } // Balance summary @@ -159,141 +170,144 @@ var body: some View { } } - // Active addresses - HStack { - Text("Active Addresses") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - Text("\(platformBalanceSyncService.activeAddressCount)") - .font(.subheadline) - .fontWeight(.medium) - } - - // Chain tip height - if platformBalanceSyncService.chainTipHeight > 0 { + // Expanded details + if showPlatformDetails { + // Active addresses HStack { - Text("Chain Tip Height") + Text("Active Addresses") .font(.subheadline) .foregroundColor(.secondary) Spacer() - Text(formattedHeight(UInt32(platformBalanceSyncService.chainTipHeight))) + Text("\(platformBalanceSyncService.activeAddressCount)") .font(.subheadline) .fontWeight(.medium) } - } - // Sync checkpoint (from tree scan) - if platformBalanceSyncService.checkpointHeight > 0 { - HStack { - Text("Sync Checkpoint") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - Text(formattedHeight(UInt32(platformBalanceSyncService.checkpointHeight))) - .font(.subheadline) - .foregroundColor(.secondary) + // Chain tip height + if platformBalanceSyncService.chainTipHeight > 0 { + HStack { + Text("Chain Tip Height") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text(formattedHeight(UInt32(platformBalanceSyncService.chainTipHeight))) + .font(.subheadline) + .fontWeight(.medium) + } } - } - // Last known recent block (for compaction detection) - HStack { - Text("Last Recent Block") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - if platformBalanceSyncService.lastKnownRecentBlock > 0 { - Text(formattedHeight(UInt32(platformBalanceSyncService.lastKnownRecentBlock))) - .font(.subheadline) - .foregroundColor(.secondary) - } else { - Text("None found") - .font(.subheadline) - .foregroundColor(.blue) - .onTapGesture { - showProofDetail = true - } + // Sync checkpoint (from tree scan) + if platformBalanceSyncService.checkpointHeight > 0 { + HStack { + Text("Sync Checkpoint") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text(formattedHeight(UInt32(platformBalanceSyncService.checkpointHeight))) + .font(.subheadline) + .foregroundColor(.secondary) + } } - } - // Block time - if let blockTime = platformBalanceSyncService.lastSyncBlockTime { + // Last known recent block (for compaction detection) HStack { - Text("Block Time") + Text("Last Recent Block") .font(.subheadline) .foregroundColor(.secondary) Spacer() - Text(blockTime, style: .date) - .font(.caption) - .foregroundColor(.secondary) - Text(blockTime, style: .time) - .font(.caption) - .foregroundColor(.secondary) + if platformBalanceSyncService.lastKnownRecentBlock > 0 { + Text(formattedHeight(UInt32(platformBalanceSyncService.lastKnownRecentBlock))) + .font(.subheadline) + .foregroundColor(.secondary) + } else { + Text("None found") + .font(.subheadline) + .foregroundColor(.blue) + .onTapGesture { + showProofDetail = true + } + } } - } - // Query counts since launch - if platformBalanceSyncService.syncCountSinceLaunch > 0 { - let svc = platformBalanceSyncService - VStack(spacing: 4) { + // Block time + if let blockTime = platformBalanceSyncService.lastSyncBlockTime { HStack { - Text("Queries Since Launch") + Text("Block Time") .font(.subheadline) .foregroundColor(.secondary) Spacer() - Text("\(svc.syncCountSinceLaunch) syncs") + Text(blockTime, style: .date) + .font(.caption) + .foregroundColor(.secondary) + Text(blockTime, style: .time) .font(.caption) .foregroundColor(.secondary) } - HStack(spacing: 12) { - QueryCountBadge(label: "Trunk", count: svc.totalTrunkQueries, color: .blue) - QueryCountBadge(label: "Branch", count: svc.totalBranchQueries, color: .indigo) - QueryCountBadge(label: "Compacted", count: svc.totalCompactedQueries, detail: svc.totalCompactedEntries, color: .orange) - QueryCountBadge(label: "Recent", count: svc.totalRecentQueries, detail: svc.totalRecentEntries, color: .green) + } + + // Query counts since launch + if platformBalanceSyncService.syncCountSinceLaunch > 0 { + let svc = platformBalanceSyncService + VStack(spacing: 4) { + HStack { + Text("Queries Since Launch") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text("\(svc.syncCountSinceLaunch) syncs") + .font(.caption) + .foregroundColor(.secondary) + } + HStack(spacing: 12) { + QueryCountBadge(label: "Trunk", count: svc.totalTrunkQueries, color: .blue) + QueryCountBadge(label: "Branch", count: svc.totalBranchQueries, color: .indigo) + QueryCountBadge(label: "Compacted", count: svc.totalCompactedQueries, detail: svc.totalCompactedEntries, color: .orange) + QueryCountBadge(label: "Recent", count: svc.totalRecentQueries, detail: svc.totalRecentEntries, color: .green) + } } } + + // Action buttons + HStack { + Spacer() + + Button { + Task { + await unifiedAppState.performPlatformBalanceSync() + } + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.clockwise") + Text("Sync Now") + } + .font(.caption) + .fontWeight(.medium) + } + .buttonStyle(.borderedProminent) + .tint(.blue) + .controlSize(.mini) + .disabled(platformBalanceSyncService.isSyncing) + + Button { + platformBalanceSyncService.reset() + } label: { + Text("Clear") + .font(.caption) + .fontWeight(.medium) + } + .buttonStyle(.borderedProminent) + .tint(.red) + .controlSize(.mini) + } } - // Error display + // Error display (always visible) if let error = platformBalanceSyncService.lastError { Text(error) .font(.caption) .foregroundColor(.red) .lineLimit(2) } - - // Action buttons - HStack { - Spacer() - - Button { - Task { - await unifiedAppState.performPlatformBalanceSync() - } - } label: { - HStack(spacing: 4) { - Image(systemName: "arrow.clockwise") - Text("Sync Now") - } - .font(.caption) - .fontWeight(.medium) - } - .buttonStyle(.borderedProminent) - .tint(.blue) - .controlSize(.mini) - .disabled(platformBalanceSyncService.isSyncing) - - Button { - platformBalanceSyncService.reset() - } label: { - Text("Clear") - .font(.caption) - .fontWeight(.medium) - } - .buttonStyle(.borderedProminent) - .tint(.red) - .controlSize(.mini) - } } .padding(.vertical, 4) } header: { @@ -327,6 +341,15 @@ var body: some View { .foregroundColor(.secondary) } Spacer() + // Expand/collapse chevron + Button { + showZKDetails.toggle() + } label: { + Image(systemName: showZKDetails ? "chevron.up" : "chevron.down") + .font(.caption) + .foregroundColor(.secondary) + } + .buttonStyle(.plain) } // Shielded balance @@ -346,93 +369,96 @@ var body: some View { } } - // Orchard address (truncated) - if let address = zkSyncService.orchardAddress { + // Expanded details + if showZKDetails { + // Orchard address (truncated) + if let address = zkSyncService.orchardAddress { + HStack { + Text("Orchard Address") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text(String(address.prefix(12)) + "..." + String(address.suffix(6))) + .foregroundColor(.secondary) + .font(.system(.caption, design: .monospaced)) + } + } + + // Last sync stats HStack { - Text("Orchard Address") + Text("Last Sync") .font(.subheadline) .foregroundColor(.secondary) Spacer() - Text(String(address.prefix(12)) + "..." + String(address.suffix(6))) + Text("\(zkSyncService.notesSynced) notes, \(zkSyncService.nullifiersSpent) spent") + .font(.caption) .foregroundColor(.secondary) - .font(.system(.caption, design: .monospaced)) } - } - // Last sync stats - HStack { - Text("Last Sync") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - Text("\(zkSyncService.notesSynced) notes, \(zkSyncService.nullifiersSpent) spent") - .font(.caption) - .foregroundColor(.secondary) - } + // Cumulative totals + HStack { + Text("Total Synced") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text("\(zkSyncService.totalNotesSynced) notes, \(zkSyncService.totalNullifiersSpent) spent") + .font(.caption) + .foregroundColor(.secondary) + } - // Cumulative totals - HStack { - Text("Total Synced") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - Text("\(zkSyncService.totalNotesSynced) notes, \(zkSyncService.totalNullifiersSpent) spent") - .font(.caption) - .foregroundColor(.secondary) - } + // Sync count + HStack { + Text("Sync Count") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text("\(zkSyncService.syncCountSinceLaunch)") + .font(.caption) + .foregroundColor(.secondary) + } - // Sync count - HStack { - Text("Sync Count") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - Text("\(zkSyncService.syncCountSinceLaunch)") - .font(.caption) - .foregroundColor(.secondary) + // Action buttons + HStack { + Spacer() + + Button { + Task { + await unifiedAppState.performZKSync() + } + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.clockwise") + Text("Sync Now") + } + .font(.caption) + .fontWeight(.medium) + } + .buttonStyle(.borderedProminent) + .tint(.blue) + .controlSize(.mini) + .disabled(zkSyncService.isSyncing) + + Button { + zkSyncService.reset() + } label: { + Text("Clear") + .font(.caption) + .fontWeight(.medium) + } + .buttonStyle(.borderedProminent) + .tint(.red) + .controlSize(.mini) + .disabled(zkSyncService.isSyncing) + } } - // Error display + // Error display (always visible) if let error = zkSyncService.lastError { Text(error) .font(.caption) .foregroundColor(.red) .lineLimit(2) } - - // Action buttons - HStack { - Spacer() - - Button { - Task { - await unifiedAppState.performZKSync() - } - } label: { - HStack(spacing: 4) { - Image(systemName: "arrow.clockwise") - Text("Sync Now") - } - .font(.caption) - .fontWeight(.medium) - } - .buttonStyle(.borderedProminent) - .tint(.blue) - .controlSize(.mini) - .disabled(zkSyncService.isSyncing) - - Button { - zkSyncService.reset() - } label: { - Text("Clear") - .font(.caption) - .fontWeight(.medium) - } - .buttonStyle(.borderedProminent) - .tint(.red) - .controlSize(.mini) - .disabled(zkSyncService.isSyncing) - } } .padding(.vertical, 4) } header: { From 93205ffe06c5f0549d7192e2b7e9f904fa369d6f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 21 Mar 2026 10:37:44 +0300 Subject: [PATCH 03/27] feat(ffi): support local Docker setup with quorum sidecar for regtest - Use TrustedHttpContextProvider::new_with_url for local/regtest networks, connecting to quorum sidecar at localhost:22444 (dashmate Docker default) - Replace separate "Use Local DAPI" and "Use Local Core" toggles with unified "Use Docker Setup" toggle - Default DAPI address changed to https://127.0.0.1:2443 (envoy gateway) - Persist selected tab across app launches - SDK.init uses dash_sdk_create_trusted for all networks (Rust auto-detects local) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/rs-sdk-ffi/src/sdk.rs | 54 ++++++++++++++----- .../swift-sdk/Sources/SwiftDashSDK/SDK.swift | 20 ++----- .../SwiftExampleApp/AppState.swift | 51 +++++++++--------- .../SwiftExampleApp/ContentView.swift | 13 ++++- .../SwiftExampleApp/Views/OptionsView.swift | 13 ++--- 5 files changed, 85 insertions(+), 66 deletions(-) diff --git a/packages/rs-sdk-ffi/src/sdk.rs b/packages/rs-sdk-ffi/src/sdk.rs index 4d60eeb0ec..bfea1fe335 100644 --- a/packages/rs-sdk-ffi/src/sdk.rs +++ b/packages/rs-sdk-ffi/src/sdk.rs @@ -331,21 +331,47 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) - ); // Create trusted context provider - let trusted_provider = match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new( - network, - None, // Use default quorum lookup endpoints - std::num::NonZeroUsize::new(100).unwrap(), // Cache size - ) { - Ok(provider) => { - info!("dash_sdk_create_trusted: trusted context provider created"); - Arc::new(provider) + // For local/regtest, use the quorum sidecar at localhost:22444 (dashmate Docker default) + let is_local = matches!( + config.network, + DashSDKNetwork::SDKLocal | DashSDKNetwork::SDKRegtest + ); + let trusted_provider = if is_local { + info!("dash_sdk_create_trusted: using local quorum sidecar for regtest"); + match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new_with_url( + network, + "http://127.0.0.1:22444".to_string(), + std::num::NonZeroUsize::new(100).unwrap(), + ) { + Ok(provider) => { + info!("dash_sdk_create_trusted: local trusted context provider created"); + Arc::new(provider) + } + Err(e) => { + error!(error = %e, "dash_sdk_create_trusted: failed to create local context provider"); + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InternalError, + format!("Failed to create local context provider: {}", e), + )); + } } - Err(e) => { - error!(error = %e, "dash_sdk_create_trusted: failed to create trusted context provider"); - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InternalError, - format!("Failed to create trusted context provider: {}", e), - )); + } else { + match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new( + network, + None, // Use default quorum lookup endpoints + std::num::NonZeroUsize::new(100).unwrap(), // Cache size + ) { + Ok(provider) => { + info!("dash_sdk_create_trusted: trusted context provider created"); + Arc::new(provider) + } + Err(e) => { + error!(error = %e, "dash_sdk_create_trusted: failed to create trusted context provider"); + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InternalError, + format!("Failed to create trusted context provider: {}", e), + )); + } } }; diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift index e2459e9017..6b4f97d251 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift @@ -145,7 +145,7 @@ public final class SDK: @unchecked Sendable { if let override = UserDefaults.standard.string(forKey: "platformDAPIAddresses"), !override.isEmpty { return override } - return "http://127.0.0.1:1443" + return "https://127.0.0.1:2443" } /// Create a new SDK instance with trusted setup @@ -154,39 +154,27 @@ public final class SDK: @unchecked Sendable { /// data contracts from trusted HTTP endpoints instead of requiring proof verification. /// This is suitable for mobile applications where proof verification would be resource-intensive. public init(network: Network) throws { - print("πŸ”΅ SDK.init: Creating SDK with network: \(network)") var config = DashSDKConfig() - - // Map network - in C enums, Swift imports them as raw values config.network = network - print("πŸ”΅ SDK.init: Network config set to: \(config.network)") - - // Default to SDK-provided addresses; may override below config.dapi_addresses = nil - config.skip_asset_lock_proof_verification = false config.request_retry_count = 1 config.request_timeout_ms = 8000 // 8 seconds - // Create SDK with trusted setup - print("πŸ”΅ SDK.init: Creating SDK with trusted setup...") + // Create SDK with trusted setup β€” Rust side auto-detects local/regtest + // and uses the quorum sidecar at localhost:22444 instead of remote endpoints let result: DashSDKResult - // Force local DAPI regardless of selected network when enabled - let forceLocal = UserDefaults.standard.bool(forKey: "useLocalhostPlatform") + let forceLocal = UserDefaults.standard.bool(forKey: "useDockerSetup") if forceLocal { let localAddresses = Self.platformDAPIAddresses - print("πŸ”΅ SDK.init: Using local DAPI addresses: \(localAddresses)") result = localAddresses.withCString { addressesCStr -> DashSDKResult in var mutableConfig = config mutableConfig.dapi_addresses = addressesCStr - print("πŸ”΅ SDK.init: Calling dash_sdk_create_trusted...") return dash_sdk_create_trusted(&mutableConfig) } } else { - print("πŸ”΅ SDK.init: Using default network addresses") result = dash_sdk_create_trusted(&config) } - print("πŸ”΅ SDK.init: dash_sdk_create_trusted returned") // Check for errors if result.error != nil { diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift index a7816f3bb5..df6365d107 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift @@ -26,21 +26,20 @@ class AppState: ObservableObject { @Published var dataStatistics: (identities: Int, documents: Int, contracts: Int, tokenBalances: Int)? - @Published var useLocalPlatform: Bool { + @Published var useDockerSetup: Bool { didSet { - UserDefaults.standard.set(useLocalPlatform, forKey: "useLocalhostPlatform") - // Maintain backward-compat key for older SDK builds - UserDefaults.standard.set(useLocalPlatform, forKey: "useLocalhost") + UserDefaults.standard.set(useDockerSetup, forKey: "useDockerSetup") + // Write to legacy keys so SDK.swift and SPVClient.swift pick them up + UserDefaults.standard.set(useDockerSetup, forKey: "useLocalhostPlatform") + UserDefaults.standard.set(useDockerSetup, forKey: "useLocalhostCore") + UserDefaults.standard.set(useDockerSetup, forKey: "useLocalhost") Task { await switchNetwork(to: currentNetwork) } } } - @Published var useLocalCore: Bool { - didSet { - UserDefaults.standard.set(useLocalCore, forKey: "useLocalhostCore") - // TODO: Reconfigure SPV client peers when supported - } - } + /// Backward-compat computed properties (read-only) + var useLocalPlatform: Bool { useDockerSetup } + var useLocalCore: Bool { useDockerSetup } private let testSigner = TestSigner() private var dataManager: DataManager? @@ -54,12 +53,17 @@ class AppState: ObservableObject { } else { self.currentNetwork = .testnet } - // Migration: if legacy key set and new keys absent, propagate - let legacyLocal = UserDefaults.standard.bool(forKey: "useLocalhost") - let hasPlatformKey = UserDefaults.standard.object(forKey: "useLocalhostPlatform") != nil - let hasCoreKey = UserDefaults.standard.object(forKey: "useLocalhostCore") != nil - self.useLocalPlatform = hasPlatformKey ? UserDefaults.standard.bool(forKey: "useLocalhostPlatform") : legacyLocal - self.useLocalCore = hasCoreKey ? UserDefaults.standard.bool(forKey: "useLocalhostCore") : legacyLocal + // Migration: if legacy keys set, propagate to new unified key + if let _ = UserDefaults.standard.object(forKey: "useDockerSetup") { + self.useDockerSetup = UserDefaults.standard.bool(forKey: "useDockerSetup") + } else { + // Fall back to legacy keys + let legacyLocal = UserDefaults.standard.bool(forKey: "useLocalhostPlatform") + || UserDefaults.standard.bool(forKey: "useLocalhost") + self.useDockerSetup = legacyLocal + // Persist so SDK.swift can read it (didSet doesn't fire in init) + UserDefaults.standard.set(legacyLocal, forKey: "useDockerSetup") + } } func initializeSDK(modelContext: ModelContext) { @@ -74,21 +78,14 @@ class AppState: ObservableObject { isLoading = true NSLog("πŸ”΅ AppState: Initializing SDK library...") - // Initialize the SDK library SDK.initialize() - - // Enable debug logging to see gRPC endpoints SDK.enableLogging(level: .debug) - NSLog("πŸ”΅ AppState: Enabled debug logging for gRPC requests") - NSLog("πŸ”΅ AppState: Creating SDK instance for network: \(currentNetwork)") - // Create SDK instance for current network let sdkNetwork: DashSDKNetwork = currentNetwork.sdkNetwork - NSLog("πŸ”΅ AppState: SDK network value: \(sdkNetwork)") - + NSLog("πŸ”΅ AppState: Creating SDK for network=\(currentNetwork), docker=\(useDockerSetup)") let newSDK = try SDK(network: sdkNetwork) sdk = newSDK - NSLog("βœ… AppState: SDK created successfully with handle: \(newSDK.handle != nil ? "exists" : "nil")") + NSLog("βœ… AppState: SDK created successfully") // Load known contracts into the SDK's trusted provider await loadKnownContractsIntoSDK(sdk: newSDK, modelContext: modelContext) @@ -98,7 +95,9 @@ class AppState: ObservableObject { isLoading = false } catch { + sdk = nil showError(message: "Failed to initialize SDK: \(error.localizedDescription)") + NSLog("❌ AppState.initializeSDK: \(error)") isLoading = false } } @@ -200,7 +199,9 @@ class AppState: ObservableObject { isLoading = false } catch { + sdk = nil showError(message: "Failed to switch network: \(error.localizedDescription)") + NSLog("❌ AppState.switchNetwork: \(error)") isLoading = false } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift index 80ae6121f4..883383edcf 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift @@ -2,7 +2,7 @@ import SwiftUI import SwiftDashSDK import SwiftData -enum RootTab: Hashable { +enum RootTab: String, Hashable { case sync, wallets, friends, platform, settings } @@ -10,7 +10,13 @@ struct ContentView: View { @EnvironmentObject var unifiedState: UnifiedAppState @EnvironmentObject var walletService: WalletService - @State private var selectedTab: RootTab = .sync + @State private var selectedTab: RootTab = { + if let saved = UserDefaults.standard.string(forKey: "selectedTab"), + let tab = RootTab(rawValue: saved) { + return tab + } + return .sync + }() var body: some View { if !unifiedState.isInitialized { @@ -82,6 +88,9 @@ struct ContentView: View { } .tag(RootTab.settings) } + .onChange(of: selectedTab) { _, newTab in + UserDefaults.standard.set(newTab.rawValue, forKey: "selectedTab") + } .overlay(alignment: .top) { if walletService.syncProgress.state.isSyncing() { GlobalSyncIndicator(showDetails: selectedTab == .sync && unifiedState.showWalletsSyncDetails) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift index 89258fca73..2451a23b2f 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift @@ -39,21 +39,15 @@ struct OptionsView: View { .pickerStyle(SegmentedPickerStyle()) .disabled(isSwitchingNetwork) - Toggle("Use Local DAPI (Platform)", isOn: $appState.useLocalPlatform) - .onChange(of: appState.useLocalPlatform) { _, _ in + Toggle("Use Docker Setup", isOn: $appState.useDockerSetup) + .onChange(of: appState.useDockerSetup) { _, _ in isSwitchingNetwork = true Task { await appState.switchNetwork(to: appState.currentNetwork) await MainActor.run { isSwitchingNetwork = false } } } - .help("When enabled, Platform requests use local DAPI at 127.0.0.1:1443 (override via 'platformDAPIAddresses').") - - Toggle("Use Local Core (SPV)", isOn: $appState.useLocalCore) - .onChange(of: appState.useLocalCore) { _, _ in - // Core override will be applied when SPV peer overrides are supported - } - .help("When enabled, Core (SPV) connects only to configured peers (default 127.0.0.1 with network port). Override via 'corePeerAddresses'.") + .help("Connect to local dashmate Docker network (DAPI at 127.0.0.1:1443, Core peers at 127.0.0.1). Override addresses via 'platformDAPIAddresses' and 'corePeerAddresses' UserDefaults keys.") HStack { Text("Network Status") @@ -76,6 +70,7 @@ struct OptionsView: View { .foregroundColor(.red) } } + } Section("Data") { From 55008db62b5fd45bc716bdef25464b6aceb848aa Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 21 Mar 2026 11:45:33 +0300 Subject: [PATCH 04/27] feat(swift-sdk): add account creation UI, regtest SPV config, and new account types - Restore AddAccountView from feat/platformSync branch for adding accounts to wallets - Add platformPayment, dashPayReceivingFunds, dashPayExternalAccount to AccountCategory and AccountType - Use wallet_add_platform_payment_account() for Platform Payment accounts (requires key_class) - Add CoreWalletManager.addAccount() public method - Add regtest (case 2) to SPV client network config instead of falling through to testnet - Default local Core P2P peer to 127.0.0.1:20001 (Docker dashmate port) - Add Local/Regtest option to wallet creation network picker Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Models/HDWalletModels.swift | 3 + .../SwiftDashSDK/Core/SPV/SPVClient.swift | 7 +- .../Core/Wallet/CoreWalletManager.swift | 23 +- .../KeyWallet/KeyWalletTypes.swift | 3 + .../Core/Views/AccountListView.swift | 30 +- .../Core/Views/AddAccountView.swift | 319 ++++++++++++++++++ .../Core/Views/CreateWalletView.swift | 21 +- 7 files changed, 398 insertions(+), 8 deletions(-) create mode 100644 packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddAccountView.swift diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/HDWalletModels.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/HDWalletModels.swift index 070a9d2ae0..fb2fa1d6b5 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/HDWalletModels.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/HDWalletModels.swift @@ -21,6 +21,9 @@ public enum AccountCategory: Equatable, Hashable, Sendable { case providerOwnerKeys case providerOperatorKeys case providerPlatformKeys + case dashPayReceivingFunds + case dashPayExternalAccount + case platformPayment } // MARK: - Account Info diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift index a33193153b..c53c915455 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift @@ -47,6 +47,9 @@ class SPVClient: @unchecked Sendable { return dash_spv_ffi_config_mainnet() case 1: return dash_spv_ffi_config_testnet() + case 2: + // Regtest (local Docker) + return dash_spv_ffi_config_new(FFINetwork(rawValue: 2)) case 3: // Map devnet to custom FFINetwork value 3 return dash_spv_ffi_config_new(FFINetwork(rawValue: 3)) @@ -134,9 +137,9 @@ class SPVClient: @unchecked Sendable { } private static func readLocalCorePeers() -> [String] { - // If no override is set, default to 127.0.0.1 and let FFI pick port by network + // If no override is set, default to dashmate Docker Core P2P port let raw = UserDefaults.standard.string(forKey: "corePeerAddresses")?.trimmingCharacters(in: .whitespacesAndNewlines) - let list = (raw?.isEmpty == false ? raw! : "127.0.0.1") + let list = (raw?.isEmpty == false ? raw! : "127.0.0.1:20001") return list .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespaces) } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index c9535a7e20..aa0d3e05ab 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -105,6 +105,18 @@ public class CoreWalletManager: ObservableObject { return wallet } + /// Add a new account to a wallet. + public func addAccount(to wallet: HDWallet, type: AccountType, index: UInt32, keyClass: UInt32 = 0) throws { + guard let sdkWallet = try sdkWalletManager.getWallet(id: wallet.walletId) else { + throw WalletError.walletError("Wallet not found") + } + if type == .platformPayment { + try sdkWallet.addPlatformPaymentAccount(accountIndex: index, keyClass: keyClass) + } else { + _ = try sdkWallet.addAccount(type: type, index: index) + } + } + public func deleteWallet(_ wallet: HDWallet) async throws { let walletId = wallet.id @@ -244,6 +256,8 @@ public class CoreWalletManager: ObservableObject { managed = collection.getProviderOperatorKeysAccount() case .providerPlatformKeys: managed = collection.getProviderPlatformKeysAccount() + case .dashPayReceivingFunds, .dashPayExternalAccount, .platformPayment: + managed = nil // TODO: implement when FFI supports these account types } let appNetwork = AppNetwork(network: sdkWalletManager.network) @@ -316,7 +330,8 @@ public class CoreWalletManager: ObservableObject { case .coinjoin: let idx = (accountInfo.index ?? 1000) - 1000 return (.coinJoin, UInt32(idx), "m/9'/\(coinType)/4'/\(idx)'") - case .identityRegistration, .identityInvitation, .identityTopupNotBound, .identityTopup: + case .identityRegistration, .identityInvitation, .identityTopupNotBound, .identityTopup, + .dashPayReceivingFunds, .dashPayExternalAccount, .platformPayment: return nil } }() @@ -360,6 +375,12 @@ public class CoreWalletManager: ObservableObject { return "m/9'/\(coinType)/3'/3'/x" case .providerPlatformKeys: return "m/9'/\(coinType)/3'/4'/x" + case .dashPayReceivingFunds: + return "m/9'/\(coinType)/5'/0'/x" + case .dashPayExternalAccount: + return "m/9'/\(coinType)/5'/0'/x" + case .platformPayment: + return "m/9'/\(coinType)/15'/\(index ?? 0)'/x" } } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWalletTypes.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWalletTypes.swift index 2e6cb51d39..a1da7ca852 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWalletTypes.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWalletTypes.swift @@ -45,6 +45,9 @@ public enum AccountType: UInt32 { case providerOwnerKeys = 8 case providerOperatorKeys = 9 case providerPlatformKeys = 10 + case dashPayReceivingFunds = 11 + case dashPayExternalAccount = 12 + case platformPayment = 13 var ffiValue: FFIAccountType { FFIAccountType(rawValue: self.rawValue) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift index ab14a281b0..0e8abb6e0f 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift @@ -9,6 +9,7 @@ struct AccountListView: View { @EnvironmentObject var walletService: WalletService let wallet: HDWallet @State private var accounts: [AccountInfo] = [] + @State private var showAddAccount = false var body: some View { ZStack { @@ -29,7 +30,22 @@ struct AccountListView: View { loadAccounts() } } - }.task { + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + showAddAccount = true + } label: { + Image(systemName: "plus") + } + } + } + .sheet(isPresented: $showAddAccount) { + AddAccountView(wallet: wallet) + .environmentObject(walletService) + .onDisappear { loadAccounts() } + } + .task { loadAccounts() } } @@ -46,7 +62,7 @@ struct AccountRowView: View { /// Determines if this account type should show balance in UI var shouldShowBalance: Bool { switch account.category { - case .bip44, .bip32, .coinjoin: + case .bip44, .bip32, .coinjoin, .platformPayment: return true default: return false @@ -65,7 +81,10 @@ struct AccountRowView: View { case .providerVotingKeys: return "Voting" case .providerOwnerKeys: return "Owner" case .providerOperatorKeys: return "Operator" - case .providerPlatformKeys: return "Platform" + case .providerPlatformKeys: return "Platform Keys" + case .dashPayReceivingFunds: return "DashPay" + case .dashPayExternalAccount: return "DashPay Ext" + case .platformPayment: return account.index.map { "Payment #\($0)" } ?? "Payment" } } @@ -81,6 +100,9 @@ struct AccountRowView: View { case .providerOwnerKeys: return "key.horizontal" case .providerOperatorKeys: return "wrench.and.screwdriver" case .providerPlatformKeys: return "network" + case .dashPayReceivingFunds: return "person.2.circle" + case .dashPayExternalAccount: return "person.crop.circle.badge.questionmark" + case .platformPayment: return "creditcard.fill" } } @@ -94,6 +116,8 @@ struct AccountRowView: View { case .providerOwnerKeys: return .pink case .providerOperatorKeys: return .indigo case .providerPlatformKeys: return .teal + case .dashPayReceivingFunds, .dashPayExternalAccount: return .cyan + case .platformPayment: return .green } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddAccountView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddAccountView.swift new file mode 100644 index 0000000000..1f835a4502 --- /dev/null +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddAccountView.swift @@ -0,0 +1,319 @@ +import SwiftUI +import SwiftDashSDK +import SwiftData + +/// View for adding a new account to a wallet +struct AddAccountView: View { + @EnvironmentObject var walletService: WalletService + @Environment(\.dismiss) private var dismiss + + let wallet: HDWallet + + @State private var selectedAccountType: AddableAccountType = .bip44 + @State private var accountIndex: String = "" + @State private var keyClass: String = "0" + @State private var isCreating = false + @State private var errorMessage: String? + @State private var showError = false + + /// Account types that can be added by the user + enum AddableAccountType: String, CaseIterable, Identifiable { + case bip44 = "BIP44 (Standard)" + case bip32 = "BIP32 (Legacy)" + case coinjoin = "CoinJoin (Privacy)" + case platformPayment = "Platform Payment" + case identityTopup = "Identity Top-up" + + var id: String { rawValue } + + var accountType: AccountType { + switch self { + case .bip44: return .standardBIP44 + case .bip32: return .standardBIP32 + case .coinjoin: return .coinJoin + case .platformPayment: return .platformPayment + case .identityTopup: return .identityTopUp + } + } + + var description: String { + switch self { + case .bip44: + return "Standard account for receiving and sending DASH. Recommended for most users." + case .bip32: + return "Legacy account type for compatibility with older systems." + case .coinjoin: + return "Privacy-enhanced account for mixing transactions." + case .platformPayment: + return "Platform payment account (DIP-17) for receiving credits to platform payment addresses." + case .identityTopup: + return "Account for topping up platform identity credits." + } + } + + var icon: String { + switch self { + case .bip44: return "folder.fill" + case .bip32: return "tray.full.fill" + case .coinjoin: return "shuffle.circle.fill" + case .platformPayment: return "creditcard.fill" + case .identityTopup: return "arrow.up.circle.fill" + } + } + + var color: Color { + switch self { + case .bip44: return .blue + case .bip32: return .teal + case .coinjoin: return .orange + case .platformPayment: return .green + case .identityTopup: return .purple + } + } + + var requiresIndex: Bool { + // All these account types require an index + return true + } + + var indexPlaceholder: String { + switch self { + case .bip44: return "e.g., 1, 2, 3..." + case .bip32: return "e.g., 0, 1, 2..." + case .coinjoin: return "e.g., 0, 1, 2..." + case .platformPayment: return "e.g., 0, 1, 2..." + case .identityTopup: return "Identity index (e.g., 0)" + } + } + + /// Returns true if this account type needs a key class parameter + var requiresKeyClass: Bool { + self == .platformPayment + } + + /// Returns the derivation path template for this account type + func derivationPath(index: UInt32, keyClass: UInt32, isTestnet: Bool) -> String { + let coinType = isTestnet ? "1'" : "5'" + switch self { + case .bip44: + return "m/44'/\(coinType)/\(index)'" + case .bip32: + return "m/\(index)'" + case .coinjoin: + return "m/9'/\(coinType)/4'/\(index)'" + case .platformPayment: + return "m/9'/\(coinType)/17'/\(index)'/\(keyClass)'/..." + case .identityTopup: + return "m/9'/\(coinType)/5'/2'/\(index)'/..." + } + } + } + + var body: some View { + NavigationView { + Form { + // Account Type Selection + Section { + Picker("Account Type", selection: $selectedAccountType) { + ForEach(AddableAccountType.allCases) { type in + HStack { + Image(systemName: type.icon) + .foregroundColor(type.color) + Text(type.rawValue) + } + .tag(type) + } + } + .pickerStyle(.navigationLink) + } header: { + Text("Account Type") + } footer: { + Text(selectedAccountType.description) + .foregroundColor(.secondary) + } + + // Account Index + Section { + TextField(selectedAccountType.indexPlaceholder, text: $accountIndex) + .keyboardType(.numberPad) + } header: { + Text("Account Index") + } footer: { + Text("Enter the account index number. Each account type can have multiple accounts with different indices.") + .foregroundColor(.secondary) + } + + // Key Class (for Platform Payment accounts) + if selectedAccountType.requiresKeyClass { + Section { + TextField("e.g., 0", text: $keyClass) + .keyboardType(.numberPad) + } header: { + Text("Key Class") + } footer: { + Text("The key class level in the DIP-17 derivation path. Typically 0 for main addresses.") + .foregroundColor(.secondary) + } + } + + // Preview + Section("Preview") { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: selectedAccountType.icon) + .foregroundColor(selectedAccountType.color) + .font(.title2) + + VStack(alignment: .leading, spacing: 4) { + Text(accountLabel) + .font(.headline) + + Text(selectedAccountType.rawValue) + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + + if let index = parsedIndex { + Text("#\(index)") + .font(.system(.body, design: .monospaced)) + .foregroundColor(.secondary) + } + } + + // Derivation Path + if parsedIndex != nil { + HStack { + Text("Path:") + .font(.caption) + .foregroundColor(.secondary) + Text(derivationPath) + .font(.system(.caption, design: .monospaced)) + .foregroundColor(.secondary) + } + } + } + .padding(.vertical, 4) + } + + // Create Button + Section { + Button(action: createAccount) { + HStack { + Spacer() + if isCreating { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(0.8) + Text("Creating...") + } else { + Image(systemName: "plus.circle.fill") + Text("Create Account") + } + Spacer() + } + } + .disabled(!canCreate || isCreating) + } + } + .navigationTitle("Add Account") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + } + .alert("Error", isPresented: $showError) { + Button("OK") { } + } message: { + Text(errorMessage ?? "An unknown error occurred") + } + } + } + + // MARK: - Computed Properties + + private var parsedIndex: UInt32? { + guard !accountIndex.isEmpty else { return nil } + return UInt32(accountIndex) + } + + private var parsedKeyClass: UInt32 { + UInt32(keyClass) ?? 0 + } + + private var canCreate: Bool { + parsedIndex != nil + } + + private var isTestnet: Bool { + wallet.network == .testnet || wallet.network == .regtest || wallet.network == .devnet + } + + private var derivationPath: String { + guard let index = parsedIndex else { return "" } + return selectedAccountType.derivationPath(index: index, keyClass: parsedKeyClass, isTestnet: isTestnet) + } + + private var accountLabel: String { + guard let index = parsedIndex else { + return "Account" + } + + switch selectedAccountType { + case .bip44: + return index == 0 ? "Main Account" : "Account \(index)" + case .bip32: + return "BIP32 Account \(index)" + case .coinjoin: + return "CoinJoin Account \(index)" + case .platformPayment: + return "Platform Payment \(index)" + case .identityTopup: + return "Top-up Account \(index)" + } + } + + // MARK: - Actions + + private func createAccount() { + guard let index = parsedIndex else { return } + + isCreating = true + errorMessage = nil + + Task { + do { + // Add the account via the wallet manager + try walletService.walletManager.addAccount( + to: wallet, + type: selectedAccountType.accountType, + index: index, + keyClass: parsedKeyClass + ) + + await MainActor.run { + isCreating = false + dismiss() + } + } catch { + await MainActor.run { + isCreating = false + errorMessage = error.localizedDescription + showError = true + } + } + } + } +} + +// MARK: - Preview + +struct AddAccountView_Previews: PreviewProvider { + static var previews: some View { + Text("Preview requires wallet context") + } +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift index 68bc0220e3..e1e7479ed4 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift @@ -38,11 +38,14 @@ struct CreateWalletView: View { unifiedAppState.platformState.currentNetwork } - // Only show devnet option if currently on devnet var shouldShowDevnet: Bool { currentNetwork == .devnet } + var shouldShowRegtest: Bool { + currentNetwork == .regtest + } + var body: some View { Form { Section { @@ -96,6 +99,19 @@ struct CreateWalletView: View { } .toggleStyle(CheckboxToggleStyle()) } + + // Only show Regtest/Local if currently on Local + if shouldShowRegtest { + Toggle(isOn: $createForRegtest) { + HStack { + Image(systemName: "network") + .foregroundColor(.purple) + Text("Local") + .font(.body) + } + } + .toggleStyle(CheckboxToggleStyle()) + } } .padding(.vertical, 4) } header: { @@ -221,7 +237,7 @@ struct CreateWalletView: View { } private var hasNetworkSelected: Bool { - createForMainnet || createForTestnet || createForDevnet + createForMainnet || createForTestnet || createForDevnet || createForRegtest } private func setupInitialNetworkSelection() { @@ -279,6 +295,7 @@ struct CreateWalletView: View { createForMainnet ? AppNetwork.mainnet : nil, createForTestnet ? AppNetwork.testnet : nil, (createForDevnet && shouldShowDevnet) ? AppNetwork.devnet : nil, + (createForRegtest && shouldShowRegtest) ? AppNetwork.regtest : nil, ].compactMap { $0 } guard let primaryNetwork = selectedNetworks.first else { From 3212d872591ab3d30f61b7204f5e7611551b3c1c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 21 Mar 2026 11:49:35 +0300 Subject: [PATCH 05/27] fix(swift-sdk): list Platform Payment accounts in wallet account view Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Wallet/CoreWalletManager.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index aa0d3e05ab..f3d4f288cc 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -467,6 +467,19 @@ public class CoreWalletManager: ObservableObject { list.append(AccountInfo(category: .providerPlatformKeys, label: "Provider Platform Keys (EdDSA)", balance: b, addressCount: (0, 0))) } + // Platform Payment (DIP-17) + if collection.hasPlatformPaymentAccounts { + for accountIdx in 0.. Date: Sat, 21 Mar 2026 11:57:31 +0300 Subject: [PATCH 06/27] fix(swift-sdk): use HTTP for local DAPI and list platform payment accounts - Change default local DAPI from https to http (Docker envoy self-signed cert rejected) - Enumerate platform payment accounts in getAccounts() for wallet detail view Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift index 6b4f97d251..5f0c88ace1 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift @@ -145,7 +145,7 @@ public final class SDK: @unchecked Sendable { if let override = UserDefaults.standard.string(forKey: "platformDAPIAddresses"), !override.isEmpty { return override } - return "https://127.0.0.1:2443" + return "http://127.0.0.1:2443" } /// Create a new SDK instance with trusted setup From 2cfe1e8272a5256d972706e2df182f8e4c5e5a2c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 21 Mar 2026 20:36:14 +0300 Subject: [PATCH 07/27] feat(swift-sdk): add local Docker faucet button and RPC password setting - Add "Get 10 DASH from Faucet" button on Receive screen (Core tab, Docker mode only) - Calls seed node Core RPC sendtoaddress at 127.0.0.1:20302 - Add Faucet RPC Password field in Settings when Docker Setup is enabled - Password persisted in UserDefaults for subsequent faucet requests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Views/ReceiveAddressView.swift | 98 +++++++++++++++++++ .../SwiftExampleApp/Views/OptionsView.swift | 12 ++- 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift index b4d677297c..321b4561c3 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift @@ -17,6 +17,8 @@ struct ReceiveAddressView: View { @State private var selectedTab: ReceiveAddressTab = .core @State private var copiedToClipboard = false + @State private var faucetStatus: String? + @State private var isFaucetLoading = false private var currentAddress: String { switch selectedTab { @@ -143,6 +145,27 @@ struct ReceiveAddressView: View { .buttonStyle(.borderedProminent) .tint(tabColor) .padding(.horizontal) + + // Faucet button β€” only on local Docker, Core tab + if selectedTab == .core && unifiedAppState.platformState.useDockerSetup { + Button { + Task { await requestFromFaucet() } + } label: { + HStack { + if isFaucetLoading { + ProgressView().scaleEffect(0.8) + } else { + Image(systemName: "drop.fill") + } + Text(faucetStatus ?? "Get 10 DASH from Faucet") + } + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .tint(.green) + .padding(.horizontal) + .disabled(isFaucetLoading) + } } else { Spacer() Text(unavailableMessage) @@ -217,4 +240,79 @@ struct ReceiveAddressView: View { copiedToClipboard = false } } + + /// Request 10 DASH from the local Docker faucet (seed node Core RPC). + private func requestFromFaucet() async { + isFaucetLoading = true + faucetStatus = nil + defer { isFaucetLoading = false } + + let address = currentAddress + guard !address.isEmpty else { + faucetStatus = "No address available" + return + } + + // Read RPC port and password from UserDefaults, with dashmate defaults + let rpcPort = UserDefaults.standard.string(forKey: "faucetRPCPort") ?? "20302" + let rpcUser = UserDefaults.standard.string(forKey: "faucetRPCUser") ?? "dashmate" + let rpcPassword = UserDefaults.standard.string(forKey: "faucetRPCPassword") ?? "dashmate" + + guard let url = URL(string: "http://127.0.0.1:\(rpcPort)/") else { + faucetStatus = "Invalid RPC URL" + return + } + + let body: [String: Any] = [ + "jsonrpc": "1.0", + "id": "faucet", + "method": "sendtoaddress", + "params": [address, 10] + ] + + guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else { + faucetStatus = "Failed to encode request" + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = jsonData + request.setValue("text/plain", forHTTPHeaderField: "Content-Type") + + let credentials = "\(rpcUser):\(rpcPassword)" + if let credData = credentials.data(using: .utf8) { + request.setValue("Basic \(credData.base64EncodedString())", forHTTPHeaderField: "Authorization") + } + + do { + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + faucetStatus = "Invalid response" + return + } + + if httpResponse.statusCode == 200 { + if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let txid = json["result"] as? String { + faucetStatus = "Sent! tx: \(txid.prefix(12))..." + } else { + faucetStatus = "Sent!" + } + } else if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { + faucetStatus = "Auth failed β€” set faucetRPCPassword in UserDefaults" + } else { + let body = String(data: data, encoding: .utf8) ?? "" + faucetStatus = "RPC error \(httpResponse.statusCode): \(body.prefix(80))" + } + } catch { + faucetStatus = "Network error: \(error.localizedDescription)" + } + + // Clear status after 5 seconds + Task { + try? await Task.sleep(nanoseconds: 5_000_000_000) + faucetStatus = nil + } + } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift index 2451a23b2f..abefd53580 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift @@ -47,7 +47,17 @@ struct OptionsView: View { await MainActor.run { isSwitchingNetwork = false } } } - .help("Connect to local dashmate Docker network (DAPI at 127.0.0.1:1443, Core peers at 127.0.0.1). Override addresses via 'platformDAPIAddresses' and 'corePeerAddresses' UserDefaults keys.") + .help("Connect to local dashmate Docker network.") + + if appState.useDockerSetup { + TextField("Faucet RPC Password", text: Binding( + get: { UserDefaults.standard.string(forKey: "faucetRPCPassword") ?? "" }, + set: { UserDefaults.standard.set($0, forKey: "faucetRPCPassword") } + )) + .font(.system(.body, design: .monospaced)) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + } HStack { Text("Network Status") From 033c509f5a1081c3ac02621be180fde2249aec31 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 26 Mar 2026 10:38:24 +0300 Subject: [PATCH 08/27] chore: update for key-wallet-manager merge into key-wallet crate - Remove key-wallet-manager workspace dependency (merged into key-wallet) - Update rs-dpp and rs-platform-wallet to use key-wallet directly - Remove dashcore "std" feature (removed upstream) - Update WalletTransactionChecker for new check_core_transaction signature (added &mut Wallet and is_from_mempool params) - Add mark_instant_send_utxos to WalletInfoInterface impl Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 41 +++++-------------- Cargo.toml | 1 - packages/rs-dpp/Cargo.toml | 4 +- packages/rs-dpp/src/lib.rs | 3 +- packages/rs-platform-wallet/Cargo.toml | 3 +- .../examples/basic_usage.rs | 2 +- packages/rs-platform-wallet/src/lib.rs | 2 +- .../wallet_info_interface.rs | 4 ++ .../wallet_transaction_checker.rs | 5 ++- 9 files changed, 23 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59966e6180..aa73fe501f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1630,7 +1630,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e#42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8cbae416458565faac21d3452fbc6d80b324f6d3#8cbae416458565faac21d3452fbc6d80b324f6d3" dependencies = [ "anyhow", "async-trait", @@ -1645,7 +1645,6 @@ dependencies = [ "hickory-resolver", "indexmap 2.13.0", "key-wallet", - "key-wallet-manager", "log", "rand 0.8.5", "rayon", @@ -1663,7 +1662,7 @@ dependencies = [ [[package]] name = "dash-spv-ffi" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e#42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8cbae416458565faac21d3452fbc6d80b324f6d3#8cbae416458565faac21d3452fbc6d80b324f6d3" dependencies = [ "cbindgen 0.29.2", "clap", @@ -1673,7 +1672,6 @@ dependencies = [ "hex", "key-wallet", "key-wallet-ffi", - "key-wallet-manager", "libc", "log", "once_cell", @@ -1688,7 +1686,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e#42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8cbae416458565faac21d3452fbc6d80b324f6d3#8cbae416458565faac21d3452fbc6d80b324f6d3" dependencies = [ "anyhow", "base64-compat", @@ -1713,12 +1711,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e#42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8cbae416458565faac21d3452fbc6d80b324f6d3#8cbae416458565faac21d3452fbc6d80b324f6d3" [[package]] name = "dashcore-rpc" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e#42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8cbae416458565faac21d3452fbc6d80b324f6d3#8cbae416458565faac21d3452fbc6d80b324f6d3" dependencies = [ "dashcore-rpc-json", "hex", @@ -1731,7 +1729,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e#42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8cbae416458565faac21d3452fbc6d80b324f6d3#8cbae416458565faac21d3452fbc6d80b324f6d3" dependencies = [ "bincode", "dashcore", @@ -1746,7 +1744,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e#42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8cbae416458565faac21d3452fbc6d80b324f6d3#8cbae416458565faac21d3452fbc6d80b324f6d3" dependencies = [ "bincode", "dashcore-private", @@ -1956,7 +1954,6 @@ dependencies = [ "json-schema-compatibility-validator", "jsonschema", "key-wallet", - "key-wallet-manager", "lazy_static", "log", "nohash-hasher", @@ -3829,7 +3826,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e#42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8cbae416458565faac21d3452fbc6d80b324f6d3#8cbae416458565faac21d3452fbc6d80b324f6d3" dependencies = [ "aes", "async-trait", @@ -3845,11 +3842,13 @@ dependencies = [ "getrandom 0.2.17", "hex", "rand 0.8.5", + "rayon", "scrypt", "secp256k1", "serde", "serde_json", "sha2", + "tokio", "tracing", "zeroize", ] @@ -3857,34 +3856,17 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e#42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8cbae416458565faac21d3452fbc6d80b324f6d3#8cbae416458565faac21d3452fbc6d80b324f6d3" dependencies = [ "cbindgen 0.29.2", "dashcore", "hex", "key-wallet", - "key-wallet-manager", "libc", "secp256k1", "tokio", ] -[[package]] -name = "key-wallet-manager" -version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e#42eb1d698d4f178d3a8a80c72c9c0f9bbeddcc3e" -dependencies = [ - "async-trait", - "bincode", - "dashcore", - "dashcore_hashes", - "key-wallet", - "rayon", - "secp256k1", - "tokio", - "zeroize", -] - [[package]] name = "keyword-search-contract" version = "3.1.0-dev.1" @@ -4876,7 +4858,6 @@ dependencies = [ "dpp", "indexmap 2.13.0", "key-wallet", - "key-wallet-manager", "platform-encryption", "rand 0.8.5", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index 4173fa820d..25dfae22bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,6 @@ dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "8cbae41645 dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "8cbae416458565faac21d3452fbc6d80b324f6d3" } dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "8cbae416458565faac21d3452fbc6d80b324f6d3" } key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "8cbae416458565faac21d3452fbc6d80b324f6d3" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "8cbae416458565faac21d3452fbc6d80b324f6d3" } dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "8cbae416458565faac21d3452fbc6d80b324f6d3" } # Optimize heavy crypto crates even in dev/test builds so that diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index 1eecb169b4..1fcf55b486 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -25,7 +25,6 @@ chrono = { version = "0.4.35", default-features = false, features = [ chrono-tz = { version = "0.8", optional = true } ciborium = { version = "0.2.2", optional = true } dashcore = { workspace = true, features = [ - "std", "secp-recovery", "rand", "signer", @@ -33,7 +32,6 @@ dashcore = { workspace = true, features = [ "eddsa", ], default-features = false } key-wallet = { workspace = true, optional = true } -key-wallet-manager = { workspace = true, optional = true } dash-spv = { workspace = true, optional = true } dashcore-rpc = { workspace = true, optional = true } @@ -94,7 +92,7 @@ core_quorum_validation = ["dashcore/quorum_validation"] core_key_wallet = ["dep:key-wallet"] core_key_wallet_bincode = ["dep:key-wallet", "key-wallet/bincode"] core_key_wallet_bip_38 = ["dep:key-wallet", "key-wallet/bip38"] -core_key_wallet_manager = ["dep:key-wallet-manager"] +core_key_wallet_manager = ["dep:key-wallet"] core_key_wallet_serde = ["dep:key-wallet", "key-wallet/serde"] core_spv = ["dep:dash-spv"] core_rpc_client = ["dep:dashcore-rpc"] diff --git a/packages/rs-dpp/src/lib.rs b/packages/rs-dpp/src/lib.rs index e73fb05c25..699fcef4e0 100644 --- a/packages/rs-dpp/src/lib.rs +++ b/packages/rs-dpp/src/lib.rs @@ -12,8 +12,7 @@ pub use dashcore; #[cfg(feature = "core_key_wallet")] pub use key_wallet; -#[cfg(feature = "core_key_wallet_manager")] -pub use key_wallet_manager; +// key_wallet_manager was merged into key_wallet crate #[cfg(feature = "core_spv")] pub use dash_spv; diff --git a/packages/rs-platform-wallet/Cargo.toml b/packages/rs-platform-wallet/Cargo.toml index c30e7e43e9..391a659097 100644 --- a/packages/rs-platform-wallet/Cargo.toml +++ b/packages/rs-platform-wallet/Cargo.toml @@ -14,7 +14,6 @@ platform-encryption = { path = "../rs-platform-encryption" } # Key wallet dependencies (from rust-dashcore) key-wallet = { workspace = true } -key-wallet-manager = { workspace = true, optional = true } # Core dependencies dashcore = { workspace = true } @@ -34,4 +33,4 @@ rand = "0.8" default = ["bls", "eddsa", "manager"] bls = ["key-wallet/bls"] eddsa = ["key-wallet/eddsa"] -manager = ["key-wallet-manager"] +manager = [] diff --git a/packages/rs-platform-wallet/examples/basic_usage.rs b/packages/rs-platform-wallet/examples/basic_usage.rs index ccf3c74302..10a6b05370 100644 --- a/packages/rs-platform-wallet/examples/basic_usage.rs +++ b/packages/rs-platform-wallet/examples/basic_usage.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), PlatformWalletError> { // The platform wallet can be used with WalletManager (requires "manager" feature) #[cfg(feature = "manager")] { - use key_wallet_manager::wallet_manager::WalletManager; + use key_wallet::manager::wallet_manager::WalletManager; let _wallet_manager = WalletManager::::new(network); println!("Platform wallet successfully integrated with wallet managers!"); diff --git a/packages/rs-platform-wallet/src/lib.rs b/packages/rs-platform-wallet/src/lib.rs index 694bbb2d14..f1c96edda7 100644 --- a/packages/rs-platform-wallet/src/lib.rs +++ b/packages/rs-platform-wallet/src/lib.rs @@ -22,4 +22,4 @@ pub use managed_identity::ManagedIdentity; pub use platform_wallet_info::PlatformWalletInfo; #[cfg(feature = "manager")] -pub use key_wallet_manager; +pub use key_wallet; diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/wallet_info_interface.rs b/packages/rs-platform-wallet/src/platform_wallet_info/wallet_info_interface.rs index cb3ccead40..e88b9a1b5d 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/wallet_info_interface.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/wallet_info_interface.rs @@ -111,4 +111,8 @@ impl WalletInfoInterface for PlatformWalletInfo { fn update_synced_height(&mut self, current_height: u32) { self.wallet_info.update_synced_height(current_height) } + + fn mark_instant_send_utxos(&mut self, txid: &dashcore::Txid) -> bool { + self.wallet_info.mark_instant_send_utxos(txid) + } } diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs b/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs index 0d711984de..7b5a2084de 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs @@ -13,13 +13,14 @@ impl WalletTransactionChecker for PlatformWalletInfo { &mut self, tx: &Transaction, context: TransactionContext, - wallet: &Wallet, + wallet: &mut Wallet, update_state: bool, + is_from_mempool: bool, ) -> TransactionCheckResult { // Check transaction with underlying wallet info let result = self .wallet_info - .check_core_transaction(tx, context, wallet, update_state) + .check_core_transaction(tx, context, wallet, update_state, is_from_mempool) .await; // If the transaction is relevant, and it's an asset lock, automatically fetch identities From f1ef279f6e08b945b8fc43f9d89ebed9d82a3faa Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 26 Mar 2026 14:19:22 +0300 Subject: [PATCH 09/27] fix(swift-sdk): only show Docker toggle on Local network, auto-disable on switch Co-Authored-By: Claude Opus 4.6 (1M context) --- .../SwiftExampleApp/Views/OptionsView.swift | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift index abefd53580..a953371e9a 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift @@ -19,6 +19,11 @@ struct OptionsView: View { if newNetwork != appState.currentNetwork { isSwitchingNetwork = true Task { + // Auto-disable Docker when leaving Local + if newNetwork != .regtest && appState.useDockerSetup { + appState.useDockerSetup = false + } + // Update platform state (which will trigger SDK switch) appState.currentNetwork = newNetwork @@ -39,24 +44,26 @@ struct OptionsView: View { .pickerStyle(SegmentedPickerStyle()) .disabled(isSwitchingNetwork) - Toggle("Use Docker Setup", isOn: $appState.useDockerSetup) - .onChange(of: appState.useDockerSetup) { _, _ in - isSwitchingNetwork = true - Task { - await appState.switchNetwork(to: appState.currentNetwork) - await MainActor.run { isSwitchingNetwork = false } + if appState.currentNetwork == .regtest { + Toggle("Use Docker Setup", isOn: $appState.useDockerSetup) + .onChange(of: appState.useDockerSetup) { _, _ in + isSwitchingNetwork = true + Task { + await appState.switchNetwork(to: appState.currentNetwork) + await MainActor.run { isSwitchingNetwork = false } + } } + .help("Connect to local dashmate Docker network.") + + if appState.useDockerSetup { + TextField("Faucet RPC Password", text: Binding( + get: { UserDefaults.standard.string(forKey: "faucetRPCPassword") ?? "" }, + set: { UserDefaults.standard.set($0, forKey: "faucetRPCPassword") } + )) + .font(.system(.body, design: .monospaced)) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() } - .help("Connect to local dashmate Docker network.") - - if appState.useDockerSetup { - TextField("Faucet RPC Password", text: Binding( - get: { UserDefaults.standard.string(forKey: "faucetRPCPassword") ?? "" }, - set: { UserDefaults.standard.set($0, forKey: "faucetRPCPassword") } - )) - .font(.system(.body, design: .monospaced)) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() } HStack { From c8d9d865d375ed78febb3145dd41928900013f95 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 29 Mar 2026 08:26:46 +0300 Subject: [PATCH 10/27] fix: revert rs-dpp and rs-platform-wallet to v3.1-dev versions Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/rs-dpp/Cargo.toml | 3 ++- packages/rs-dpp/src/lib.rs | 3 ++- packages/rs-platform-wallet/Cargo.toml | 3 ++- packages/rs-platform-wallet/src/lib.rs | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index 1fcf55b486..726bbedb8c 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -32,6 +32,7 @@ dashcore = { workspace = true, features = [ "eddsa", ], default-features = false } key-wallet = { workspace = true, optional = true } +key-wallet-manager = { workspace = true, optional = true } dash-spv = { workspace = true, optional = true } dashcore-rpc = { workspace = true, optional = true } @@ -92,7 +93,7 @@ core_quorum_validation = ["dashcore/quorum_validation"] core_key_wallet = ["dep:key-wallet"] core_key_wallet_bincode = ["dep:key-wallet", "key-wallet/bincode"] core_key_wallet_bip_38 = ["dep:key-wallet", "key-wallet/bip38"] -core_key_wallet_manager = ["dep:key-wallet"] +core_key_wallet_manager = ["dep:key-wallet-manager"] core_key_wallet_serde = ["dep:key-wallet", "key-wallet/serde"] core_spv = ["dep:dash-spv"] core_rpc_client = ["dep:dashcore-rpc"] diff --git a/packages/rs-dpp/src/lib.rs b/packages/rs-dpp/src/lib.rs index 699fcef4e0..e73fb05c25 100644 --- a/packages/rs-dpp/src/lib.rs +++ b/packages/rs-dpp/src/lib.rs @@ -12,7 +12,8 @@ pub use dashcore; #[cfg(feature = "core_key_wallet")] pub use key_wallet; -// key_wallet_manager was merged into key_wallet crate +#[cfg(feature = "core_key_wallet_manager")] +pub use key_wallet_manager; #[cfg(feature = "core_spv")] pub use dash_spv; diff --git a/packages/rs-platform-wallet/Cargo.toml b/packages/rs-platform-wallet/Cargo.toml index 391a659097..c30e7e43e9 100644 --- a/packages/rs-platform-wallet/Cargo.toml +++ b/packages/rs-platform-wallet/Cargo.toml @@ -14,6 +14,7 @@ platform-encryption = { path = "../rs-platform-encryption" } # Key wallet dependencies (from rust-dashcore) key-wallet = { workspace = true } +key-wallet-manager = { workspace = true, optional = true } # Core dependencies dashcore = { workspace = true } @@ -33,4 +34,4 @@ rand = "0.8" default = ["bls", "eddsa", "manager"] bls = ["key-wallet/bls"] eddsa = ["key-wallet/eddsa"] -manager = [] +manager = ["key-wallet-manager"] diff --git a/packages/rs-platform-wallet/src/lib.rs b/packages/rs-platform-wallet/src/lib.rs index f1c96edda7..694bbb2d14 100644 --- a/packages/rs-platform-wallet/src/lib.rs +++ b/packages/rs-platform-wallet/src/lib.rs @@ -22,4 +22,4 @@ pub use managed_identity::ManagedIdentity; pub use platform_wallet_info::PlatformWalletInfo; #[cfg(feature = "manager")] -pub use key_wallet; +pub use key_wallet_manager; From a6671ff08fd360a7333d692618864f3e30375c4a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 29 Mar 2026 09:20:57 +0300 Subject: [PATCH 11/27] fix(swift-sdk): use Docker peers for SPV when useDockerSetup is enabled SPVClient was only checking useLocalhostCore key, missing useDockerSetup. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift index fbbed4752f..eeb31d5b6d 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift @@ -61,6 +61,7 @@ class SPVClient: @unchecked Sendable { // If requested, prefer local core peers (defaults to 127.0.0.1 with network default port) let useLocalCore = UserDefaults.standard.bool(forKey: "useLocalhostCore") + || UserDefaults.standard.bool(forKey: "useDockerSetup") // Only restrict to configured peers when using local core, if not, allow DNS discovery let restrictToConfiguredPeers = useLocalCore if useLocalCore { From 13c6e197357b99ffed8a2e2d5be0ee3617dd4576 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 29 Mar 2026 22:02:42 +0300 Subject: [PATCH 12/27] fix(swift-sdk): bump rust-dashcore to a1b2116 and clear default peers for Docker - Update rust-dashcore rev to a1b2116681c4f9dd (adds dash_spv_ffi_config_clear_peers) - Call clear_peers before adding Docker peers to remove default 19899 port - Fixes SPV trying to connect to non-existent 19899 peer in Docker mode Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 20 +++++++++---------- Cargo.toml | 12 +++++------ .../SwiftDashSDK/Core/SPV/SPVClient.swift | 2 ++ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3c96c1924..d8482c0d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1630,7 +1630,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f92f114b83f6e442af8290611a10f2246ee58d3a#f92f114b83f6e442af8290611a10f2246ee58d3a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" dependencies = [ "anyhow", "async-trait", @@ -1663,7 +1663,7 @@ dependencies = [ [[package]] name = "dash-spv-ffi" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f92f114b83f6e442af8290611a10f2246ee58d3a#f92f114b83f6e442af8290611a10f2246ee58d3a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" dependencies = [ "cbindgen 0.29.2", "clap", @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f92f114b83f6e442af8290611a10f2246ee58d3a#f92f114b83f6e442af8290611a10f2246ee58d3a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" dependencies = [ "anyhow", "base64-compat", @@ -1713,12 +1713,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f92f114b83f6e442af8290611a10f2246ee58d3a#f92f114b83f6e442af8290611a10f2246ee58d3a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" [[package]] name = "dashcore-rpc" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f92f114b83f6e442af8290611a10f2246ee58d3a#f92f114b83f6e442af8290611a10f2246ee58d3a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" dependencies = [ "dashcore-rpc-json", "hex", @@ -1731,7 +1731,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f92f114b83f6e442af8290611a10f2246ee58d3a#f92f114b83f6e442af8290611a10f2246ee58d3a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" dependencies = [ "bincode", "dashcore", @@ -1746,7 +1746,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f92f114b83f6e442af8290611a10f2246ee58d3a#f92f114b83f6e442af8290611a10f2246ee58d3a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" dependencies = [ "bincode", "dashcore-private", @@ -3829,7 +3829,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f92f114b83f6e442af8290611a10f2246ee58d3a#f92f114b83f6e442af8290611a10f2246ee58d3a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" dependencies = [ "aes", "async-trait", @@ -3857,7 +3857,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f92f114b83f6e442af8290611a10f2246ee58d3a#f92f114b83f6e442af8290611a10f2246ee58d3a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" dependencies = [ "cbindgen 0.29.2", "dashcore", @@ -3872,7 +3872,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f92f114b83f6e442af8290611a10f2246ee58d3a#f92f114b83f6e442af8290611a10f2246ee58d3a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" dependencies = [ "async-trait", "bincode", diff --git a/Cargo.toml b/Cargo.toml index bb5f188582..99a79754e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,12 +47,12 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "f92f114b83f6e442af8290611a10f2246ee58d3a" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "f92f114b83f6e442af8290611a10f2246ee58d3a" } -dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "f92f114b83f6e442af8290611a10f2246ee58d3a" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "f92f114b83f6e442af8290611a10f2246ee58d3a" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "f92f114b83f6e442af8290611a10f2246ee58d3a" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "f92f114b83f6e442af8290611a10f2246ee58d3a" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } +dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } # Optimize heavy crypto crates even in dev/test builds so that # Halo 2 proof generation and verification run at near-release speed. diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift index eeb31d5b6d..428665a1dc 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift @@ -69,6 +69,8 @@ class SPVClient: @unchecked Sendable { if swiftLoggingEnabled { print("[SPV][Config] Use Local Core enabled; peers=\(peers.joined(separator: ", "))") } + // Clear default peers before adding custom Docker peers + dash_spv_ffi_config_clear_peers(configPtr) // Add peers via FFI (supports "ip:port" or bare IP for network-default port) for addr in peers { addr.withCString { cstr in From c38b5a0750f02ae1f1a09201bff82ef44235b9d0 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 29 Mar 2026 22:12:34 +0300 Subject: [PATCH 13/27] fix(swift-sdk): show Platform Payment account addresses in account detail view Use ManagedPlatformAccount.getAddressPool() to populate addresses instead of returning nil for platform payment accounts. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Wallet/CoreWalletManager.swift | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index f3d4f288cc..082e56681c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -256,8 +256,11 @@ public class CoreWalletManager: ObservableObject { managed = collection.getProviderOperatorKeysAccount() case .providerPlatformKeys: managed = collection.getProviderPlatformKeysAccount() - case .dashPayReceivingFunds, .dashPayExternalAccount, .platformPayment: - managed = nil // TODO: implement when FFI supports these account types + case .dashPayReceivingFunds, .dashPayExternalAccount: + managed = nil + case .platformPayment: + // Platform Payment uses ManagedPlatformAccount, handled separately below + managed = nil } let appNetwork = AppNetwork(network: sdkWalletManager.network) @@ -266,7 +269,18 @@ public class CoreWalletManager: ObservableObject { var externalDetails: [AddressDetail] = [] var internalDetails: [AddressDetail] = [] var ffiType = FFIAccountType(rawValue: 0) - if let m = managed { + + // Special handling for Platform Payment accounts + if accountInfo.category == .platformPayment { + ffiType = FFIAccountType(rawValue: AccountType.platformPayment.rawValue) + if let platformAccount = collection.getPlatformPaymentAccount(accountIndex: accountInfo.index ?? 0, keyClass: 0), + let pool = platformAccount.getAddressPool(), + let infos = try? pool.getAddresses(from: 0, to: 0) { + externalDetails = infos.map { info in + AddressDetail(address: info.address, index: info.index, path: info.path, isUsed: info.used, publicKey: info.publicKey?.map { String(format: "%02x", $0) }.joined() ?? "") + } + } + } else if let m = managed { ffiType = FFIAccountType(rawValue: m.accountType?.rawValue ?? 0) // Query all generated addresses (0 to 0 means "all addresses" in FFI) if let pool = m.getExternalAddressPool(), let infos = try? pool.getAddresses(from: 0, to: 0) { From 87320a23be79cf502799823aa0c42e625ce5cb37 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 29 Mar 2026 22:33:42 +0300 Subject: [PATCH 14/27] fix(swift-sdk): encode Platform Payment addresses as bech32m in account detail Platform Payment addresses now display as proper DIP-17/18 bech32m (tdash1.../dash1...) instead of raw base58 P2PKH addresses. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Wallet/CoreWalletManager.swift | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index 082e56681c..2a059fe1a6 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -270,14 +270,23 @@ public class CoreWalletManager: ObservableObject { var internalDetails: [AddressDetail] = [] var ffiType = FFIAccountType(rawValue: 0) - // Special handling for Platform Payment accounts + // Special handling for Platform Payment accounts β€” encode as bech32m if accountInfo.category == .platformPayment { ffiType = FFIAccountType(rawValue: AccountType.platformPayment.rawValue) + let networkValue: UInt32 = { + switch appNetwork { + case .mainnet: return 0 + case .testnet: return 1 + case .regtest: return 2 + case .devnet: return 3 + } + }() if let platformAccount = collection.getPlatformPaymentAccount(accountIndex: accountInfo.index ?? 0, keyClass: 0), let pool = platformAccount.getAddressPool(), let infos = try? pool.getAddresses(from: 0, to: 0) { - externalDetails = infos.map { info in - AddressDetail(address: info.address, index: info.index, path: info.path, isUsed: info.used, publicKey: info.publicKey?.map { String(format: "%02x", $0) }.joined() ?? "") + externalDetails = infos.compactMap { info in + let bech32Address = Self.encodePlatformAddress(scriptPubKey: info.scriptPubKey, networkValue: networkValue) ?? info.address + return AddressDetail(address: bech32Address, index: info.index, path: info.path, isUsed: info.used, publicKey: info.publicKey?.map { String(format: "%02x", $0) }.joined() ?? "") } } } else if let m = managed { @@ -399,6 +408,23 @@ public class CoreWalletManager: ObservableObject { } + /// Encode a P2PKH scriptPubKey as a bech32m platform address (DIP-17/18). + private static func encodePlatformAddress(scriptPubKey: Data, networkValue: UInt32) -> String? { + let result = scriptPubKey.withUnsafeBytes { buffer -> DashSDKResult in + guard let base = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return DashSDKResult() + } + return dash_sdk_encode_platform_address(base, UInt32(scriptPubKey.count), networkValue) + } + guard result.error == nil, let dataPtr = result.data else { + if let error = result.error { dash_sdk_error_free(error) } + return nil + } + let str = String(cString: dataPtr.assumingMemoryBound(to: CChar.self)) + dash_sdk_string_free(dataPtr) + return str + } + // Removed old FFI-based helper; using SwiftDashSDK wrappers instead /// Get all accounts for a wallet from the FFI wallet manager From 53adb350b253ae1bebbcfcb393d60a1498e494eb Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 29 Mar 2026 23:12:41 +0300 Subject: [PATCH 15/27] fix(swift-sdk): fix bech32m HRP for platform address detection in Send view Platform addresses use "dash"/"tdash" HRP (matching Rust DPP), not "dashevo"/"tdashevo". Also recognize type byte 0x00 (P2PKH) in addition to 0xb0/0x80. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../SwiftExampleApp/Core/Models/DashAddress.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Models/DashAddress.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Models/DashAddress.swift index 82fca710ea..2638357967 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Models/DashAddress.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Models/DashAddress.swift @@ -20,18 +20,19 @@ struct DashAddress { let data = decoded.data // Check HRP validity - let validPlatformHrp = (network == .mainnet) ? "dashevo" : "tdashevo" - let validOrchardHrp = (network == .mainnet) ? "dash" : "tdash" + // Platform and Orchard share the same HRP: "dash" (mainnet) / "tdash" (testnet/regtest) + // Distinguished by type byte: 0x00/0xb0/0x80 = platform, 0x10 = orchard + let validHrp = (network == .mainnet) ? "dash" : "tdash" - if hrp == validPlatformHrp && data.count == 21 { - // Platform address: type byte 0xb0 or 0x80 + 20-byte hash + if hrp == validHrp && data.count == 21 { + // Platform address: type byte 0x00 (P2PKH) or 0xb0/0x80 + 20-byte hash let typeByte = data[0] - if typeByte == 0xb0 || typeByte == 0x80 { + if typeByte == 0x00 || typeByte == 0xb0 || typeByte == 0x80 { return DashAddress(type: .platform(data), displayString: input) } } - if hrp == validOrchardHrp && data.count >= 2 { + if hrp == validHrp && data.count >= 2 { let typeByte = data[0] if typeByte == 0x10 { // Orchard address: 0x10 type byte + 43 bytes raw address From b367564115c3ef17faefb4732a64b738ed1f44d5 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 29 Mar 2026 23:47:12 +0300 Subject: [PATCH 16/27] =?UTF-8?q?fix(swift-sdk):=20add=20Core=E2=86=92Plat?= =?UTF-8?q?form=20and=20Core=E2=86=92Core=20send=20flows,=20fix=20default?= =?UTF-8?q?=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add coreToPlatform ("Transfer to Platform") and coreToCore send flows - Default source preference to Core (most users have Core balance first) - Platform address now correctly shows "Transfer to Platform" instead of "Unshield" - TODO stubs for actual Core transfer execution Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/ViewModels/SendViewModel.swift | 27 +++++++++++++++---- .../Core/Views/SendTransactionView.swift | 2 ++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift index 1d52e08105..438f1107a1 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift @@ -3,6 +3,8 @@ import SwiftDashSDK /// Available send flow types based on source and destination. enum SendFlow: Equatable { + case coreToPlatform // Asset lock / transfer to platform address + case coreToCore // Standard Core transaction case platformToShielded // Shield credits case shieldedToShielded // Private transfer case shieldedToPlatform // Unshield @@ -10,6 +12,8 @@ enum SendFlow: Equatable { var displayName: String { switch self { + case .coreToPlatform: return "Transfer to Platform" + case .coreToCore: return "Core Transfer" case .platformToShielded: return "Shield Credits" case .shieldedToShielded: return "Shielded Transfer" case .shieldedToPlatform: return "Unshield" @@ -19,6 +23,8 @@ enum SendFlow: Equatable { var iconName: String { switch self { + case .coreToPlatform: return "arrow.up.to.line" + case .coreToCore: return "arrow.right" case .platformToShielded: return "lock.shield" case .shieldedToShielded: return "arrow.left.arrow.right" case .shieldedToPlatform: return "lock.open" @@ -26,9 +32,11 @@ enum SendFlow: Equatable { } } - /// Approximate fee in credits for this flow type. + /// Approximate fee in duffs for this flow type. var estimatedFee: UInt64 { switch self { + case .coreToPlatform: return 100_000 // ~0.001 DASH + case .coreToCore: return 100_000 // ~0.001 DASH case .platformToShielded: return 200_000 case .shieldedToShielded: return 300_000 case .shieldedToPlatform: return 300_000 @@ -58,8 +66,8 @@ class SendViewModel: ObservableObject { @Published var error: String? @Published var successMessage: String? - // Source preference (for demo UI) - @Published var preferShieldedSource = true + // Source preference (for demo UI β€” defaults to Core since shielded requires setup) + @Published var preferShieldedSource = false private let network: AppNetwork @@ -95,9 +103,10 @@ class SendViewModel: ObservableObject { case .orchard: detectedFlow = preferShieldedSource ? .shieldedToShielded : .platformToShielded case .platform: - detectedFlow = .shieldedToPlatform + // If we have shielded balance, unshield; otherwise transfer from Core + detectedFlow = preferShieldedSource ? .shieldedToPlatform : .coreToPlatform case .core: - detectedFlow = .shieldedToCore + detectedFlow = preferShieldedSource ? .shieldedToCore : .coreToCore case .unknown: detectedFlow = nil } @@ -200,6 +209,14 @@ class SendViewModel: ObservableObject { outputScript: outputScript ) successMessage = "Withdrawal submitted" + + case .coreToPlatform: + // TODO: Implement asset lock / Core β†’ Platform transfer + error = "Core to Platform transfer not yet implemented" + + case .coreToCore: + // TODO: Implement standard Core β†’ Core transaction + error = "Core to Core transfer not yet implemented" } // Refresh shielded balance diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift index 9b512ef62f..743e9fffa2 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift @@ -189,6 +189,8 @@ struct SendTransactionView: View { private func flowColor(for flow: SendFlow) -> Color { switch flow { + case .coreToPlatform: return .indigo + case .coreToCore: return .blue case .platformToShielded: return .purple case .shieldedToShielded: return .purple case .shieldedToPlatform: return .blue From e03254899e48d9258cbca02ce627adb68fea52e5 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 30 Mar 2026 10:40:23 +0300 Subject: [PATCH 17/27] fix(swift-sdk): use stored_height for filter sync progress display committed_height stays at 0 until batches are fully processed. stored_height shows actual download progress. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../SwiftExampleApp/Core/Views/CoreContentView.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift index 623995ab12..aa128c9e70 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift @@ -29,7 +29,7 @@ struct CoreContentView: View { } private var filterHeightsDisplay: String? { - let cur = walletService.syncProgress.filters?.currentHeight ?? 0 + let cur = walletService.syncProgress.filters?.storedHeight ?? 0 let tot = walletService.syncProgress.filters?.targetHeight ?? 0 return heightDisplay(numerator: cur, denominator: tot) @@ -81,7 +81,11 @@ var body: some View { CompactSyncRow( title: "Filters", - progress: walletService.syncProgress.filters?.percentage ?? 0.0, + progress: { + let stored = Double(walletService.syncProgress.filters?.storedHeight ?? 0) + let target = Double(walletService.syncProgress.filters?.targetHeight ?? 0) + return target > 0 ? stored / target : 0.0 + }(), value: filterHeightsDisplay ) From 870a0cd119b914392fabf62c1a5ec59067a9c165 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 30 Mar 2026 12:21:43 +0300 Subject: [PATCH 18/27] feat(swift-sdk): add quick-fill address buttons in Send view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "My Core" β€” own wallet receive address - "My Platform" β€” own wallet bech32m platform address - "My Shielded" β€” own wallet orchard address - Other wallet's Core address (if multiple wallets exist) - Remove "Other Options / Asset Lock Coming Soon" section Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Views/SendTransactionView.swift | 97 +++++++++++++++---- 1 file changed, 78 insertions(+), 19 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift index 743e9fffa2..a6a17d0145 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift @@ -28,6 +28,29 @@ struct SendTransactionView: View { if !viewModel.recipientAddress.isEmpty { AddressTypeBadge(type: viewModel.detectedAddressType) } + + // Quick-fill address buttons + let quickAddresses = buildQuickAddresses() + if !quickAddresses.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(quickAddresses, id: \.label) { qa in + Button { + viewModel.recipientAddress = qa.address + } label: { + Text(qa.label) + .font(.caption2) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(qa.color.opacity(0.15)) + .foregroundColor(qa.color) + .cornerRadius(12) + } + .buttonStyle(.plain) + } + } + } + } } header: { Text("Recipient") } @@ -103,25 +126,6 @@ struct SendTransactionView: View { } } - // Asset Lock (disabled) - Section { - HStack { - Image(systemName: "lock.fill") - .foregroundColor(.gray) - Text("Asset Lock") - .foregroundColor(.gray) - Spacer() - Text("Coming Soon") - .font(.caption) - .foregroundColor(.secondary) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color(UIColor.tertiarySystemBackground)) - .cornerRadius(6) - } - } header: { - Text("Other Options") - } } .navigationTitle("Send Dash") .navigationBarTitleDisplayMode(.inline) @@ -211,6 +215,61 @@ struct SendTransactionView: View { } return String(format: "%.8f DASH", dash) } + + // MARK: - Quick Address Buttons + + private struct QuickAddress { + let label: String + let address: String + let color: Color + } + + private func buildQuickAddresses() -> [QuickAddress] { + var addresses: [QuickAddress] = [] + let wallets = walletService.walletManager.wallets + + // Our wallet's internal addresses + let ownCoreAddress = walletService.walletManager.getReceiveAddress(for: wallet) + if !ownCoreAddress.isEmpty { + addresses.append(QuickAddress(label: "My Core", address: ownCoreAddress, color: .blue)) + } + + // Our platform address + if let collection = walletService.walletManager.getManagedAccountCollection(for: wallet), + let platformAccount = collection.getPlatformPaymentAccount(accountIndex: 0, keyClass: 0), + let pool = platformAccount.getAddressPool(), + let infos = try? pool.getAddresses(from: 0, to: 1), + let addrInfo = infos.first { + let networkValue: UInt32 = wallet.network == .mainnet ? 0 : 1 + let result = addrInfo.scriptPubKey.withUnsafeBytes { buffer -> DashSDKResult in + guard let base = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return DashSDKResult() + } + return dash_sdk_encode_platform_address(base, UInt32(addrInfo.scriptPubKey.count), networkValue) + } + if result.error == nil, let dataPtr = result.data { + let str = String(cString: dataPtr.assumingMemoryBound(to: CChar.self)) + dash_sdk_string_free(dataPtr) + addresses.append(QuickAddress(label: "My Platform", address: str, color: .indigo)) + } + } + + // Our shielded address + if let orchardAddress = shieldedService.orchardDisplayAddress { + addresses.append(QuickAddress(label: "My Shielded", address: orchardAddress, color: .purple)) + } + + // Other wallet's addresses (first wallet that isn't ours) + if let otherWallet = wallets.first(where: { $0.id != wallet.id }) { + let otherCore = walletService.walletManager.getReceiveAddress(for: otherWallet) + if !otherCore.isEmpty { + let name = otherWallet.label.isEmpty ? "Other" : otherWallet.label + addresses.append(QuickAddress(label: "\(name) Core", address: otherCore, color: .green)) + } + } + + return addresses + } } // MARK: - Subviews From ab5a56a8d934fe1ae2dc359dbf50cf526d592cfa Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 30 Mar 2026 20:43:23 +0300 Subject: [PATCH 19/27] chore: bump rust-dashcore to 5db46b4 (adds asset lock transaction builder FFI) Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 12 ++++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8482c0d4f..c7ddd10030 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1630,7 +1630,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" dependencies = [ "anyhow", "async-trait", @@ -1663,7 +1663,7 @@ dependencies = [ [[package]] name = "dash-spv-ffi" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" dependencies = [ "cbindgen 0.29.2", "clap", @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" dependencies = [ "anyhow", "base64-compat", @@ -1713,12 +1713,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" [[package]] name = "dashcore-rpc" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" dependencies = [ "dashcore-rpc-json", "hex", @@ -1731,7 +1731,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" dependencies = [ "bincode", "dashcore", @@ -1746,7 +1746,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" dependencies = [ "bincode", "dashcore-private", @@ -3829,7 +3829,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" dependencies = [ "aes", "async-trait", @@ -3857,7 +3857,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" dependencies = [ "cbindgen 0.29.2", "dashcore", @@ -3872,7 +3872,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c#a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" dependencies = [ "async-trait", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 99a79754e8..6497dc24bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,12 +47,12 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } -dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "a1b2116681c4f9dd04e5bbbaca352fd34dd53b0c" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } +dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } # Optimize heavy crypto crates even in dev/test builds so that # Halo 2 proof generation and verification run at near-release speed. From 7fa5afed116ccfc665950ae987b9af75b8444414 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 31 Mar 2026 04:37:53 +0300 Subject: [PATCH 20/27] feat(swift-sdk): add Swift wrapper for asset lock transaction builder - Add CoreWalletManager.buildAssetLockTransaction() wrapping wallet_build_and_sign_asset_lock_transaction FFI - Returns tx bytes, output index, one-time private key, and fee - Supports all funding types (identity reg, top-up, address top-up, etc.) - Make WalletManager.handle and Wallet.handle internal for FFI access Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Wallet/CoreWalletManager.swift | 134 ++++++++++++++++++ .../SwiftDashSDK/KeyWallet/Wallet.swift | 2 +- .../KeyWallet/WalletManager.swift | 2 +- 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index 2a059fe1a6..129a0c62c9 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -117,6 +117,140 @@ public class CoreWalletManager: ObservableObject { } } + // MARK: - Asset Lock Transaction + + /// Result of building an asset lock transaction. + public struct AssetLockTransactionResult { + /// Serialized transaction bytes. + public let transactionBytes: Data + /// Index of the asset lock output in the transaction. + public let outputIndex: UInt32 + /// One-time private key for the asset lock proof (32 bytes). + public let privateKey: Data + /// Actual fee paid in duffs. + public let fee: UInt64 + } + + /// Asset lock funding type. + public enum AssetLockFundingType: UInt32 { + case identityRegistration = 0 + case identityTopUp = 1 + case identityTopUpNotBound = 2 + case identityInvitation = 3 + case assetLockAddressTopUp = 4 + case assetLockShieldedAddressTopUp = 5 + } + + /// Build and sign an asset lock transaction for Core β†’ Platform transfers. + /// + /// Creates a Core special transaction (type 8) with AssetLockPayload that locks + /// Dash for Platform credits. + /// + /// - Parameters: + /// - wallet: The wallet to fund from. + /// - accountIndex: BIP44 account index (typically 0). + /// - fundingType: The type of asset lock funding account for key derivation. + /// - identityIndex: Identity index for key derivation (0 for new). + /// - creditOutputs: Array of (scriptPubKey, amount) pairs for platform credit outputs. + /// - feePerKb: Fee rate in duffs per kilobyte (0 for default). + /// - Returns: `AssetLockTransactionResult` with tx bytes, output index, private key, and fee. + public func buildAssetLockTransaction( + for wallet: HDWallet, + accountIndex: UInt32 = 0, + fundingType: AssetLockFundingType = .assetLockAddressTopUp, + identityIndex: UInt32 = 0, + creditOutputs: [(scriptPubKey: Data, amount: UInt64)], + feePerKb: UInt64 = 1000 + ) throws -> AssetLockTransactionResult { + guard let sdkWallet = try sdkWalletManager.getWallet(id: wallet.walletId) else { + throw WalletError.walletError("Wallet not found") + } + + let count = creditOutputs.count + guard count > 0 else { + throw WalletError.walletError("At least one credit output required") + } + + // Prepare script pointers and lengths + var scriptPtrs: [UnsafePointer?] = [] + var scriptLens: [Int] = [] + var amounts: [UInt64] = [] + + // Keep Data alive for the duration of the FFI call + let scriptDatas = creditOutputs.map { $0.scriptPubKey } + + for (i, output) in creditOutputs.enumerated() { + scriptDatas[i].withUnsafeBytes { buffer in + scriptPtrs.append(buffer.baseAddress?.assumingMemoryBound(to: UInt8.self)) + } + scriptLens.append(output.scriptPubKey.count) + amounts.append(output.amount) + } + + var feeOut: UInt64 = 0 + var txBytesOut: UnsafeMutablePointer? = nil + var txLenOut: Int = 0 + var outputIndexOut: UInt32 = 0 + var privateKeyOut: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = + (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) + var ffiError = FFIError() + + let success = scriptPtrs.withUnsafeMutableBufferPointer { scriptPtrsBuffer in + scriptLens.withUnsafeMutableBufferPointer { scriptLensBuffer in + amounts.withUnsafeMutableBufferPointer { amountsBuffer in + wallet_build_and_sign_asset_lock_transaction( + sdkWalletManager.handle, + sdkWallet.handle, + accountIndex, + fundingType.rawValue, + identityIndex, + scriptPtrsBuffer.baseAddress, + scriptLensBuffer.baseAddress, + amountsBuffer.baseAddress, + count, + feePerKb, + &feeOut, + &txBytesOut, + &txLenOut, + &outputIndexOut, + &privateKeyOut, + &ffiError + ) + } + } + } + + guard success else { + let msg = ffiError.message != nil ? String(cString: ffiError.message!) : "Unknown error" + if ffiError.message != nil { + error_message_free(ffiError.message) + } + throw WalletError.walletError("Asset lock transaction failed: \(msg)") + } + + // Copy transaction bytes + let txData: Data + if let ptr = txBytesOut, txLenOut > 0 { + txData = Data(bytes: ptr, count: txLenOut) + transaction_bytes_free(ptr) + } else { + throw WalletError.walletError("No transaction bytes returned") + } + + // Copy private key from tuple to Data + let privateKeyData = withUnsafeBytes(of: privateKeyOut) { Data($0) } + + return AssetLockTransactionResult( + transactionBytes: txData, + outputIndex: outputIndexOut, + privateKey: privateKeyData, + fee: feeOut + ) + } + public func deleteWallet(_ wallet: HDWallet) async throws { let walletId = wallet.id diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift index f97294027f..9be6cfd0c2 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift @@ -3,7 +3,7 @@ import DashSDKFFI /// Swift wrapper for a Dash wallet with HD key derivation public class Wallet { - private let handle: UnsafeMutablePointer + internal let handle: UnsafeMutablePointer private let ownsHandle: Bool // MARK: - Static Methods diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift index 93137dd4ab..ab34303e23 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift @@ -3,7 +3,7 @@ import DashSDKFFI /// Swift wrapper for wallet manager that manages multiple wallets public class WalletManager { - private let handle: UnsafeMutablePointer + internal let handle: UnsafeMutablePointer internal let network: KeyWalletNetwork private let ownsHandle: Bool From f2d908850e737cf783a02b78ae457e9531c0b34e Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 31 Mar 2026 04:48:46 +0300 Subject: [PATCH 21/27] feat(swift-sdk): add broadcastTransaction to SPVClient Wraps dash_spv_ffi_client_broadcast_transaction for broadcasting raw transactions on the Core P2P network. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../SwiftDashSDK/Core/SPV/SPVClient.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift index 00cce6ecc6..5f56aaa8df 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift @@ -196,6 +196,25 @@ class SPVClient: @unchecked Sendable { config = nil } + // MARK: - Broadcast Transactions + + func broadcastTransaction(_ transactionData: Data) throws { + try transactionData.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in + guard let txBytes = ptr.bindMemory(to: UInt8.self).baseAddress else { + throw SPVError.transactionBroadcastFailed("Invalid transaction data pointer") + } + let result = dash_spv_ffi_client_broadcast_transaction( + client, + txBytes, + UInt(transactionData.count) + ) + + if result != 0 { + throw SPVError.transactionBroadcastFailed(SPVClient.getLastDashFFIError()) + } + } + } + // MARK: - Synchronization func startSync() async throws { @@ -241,6 +260,7 @@ public enum SPVError: LocalizedError { case alreadySyncing case syncFailed(String) case storageOperationFailed(String) + case transactionBroadcastFailed(String) public var errorDescription: String? { switch self { @@ -260,6 +280,8 @@ public enum SPVError: LocalizedError { return "Sync failed: \(reason)" case let .storageOperationFailed(reason): return reason + case let .transactionBroadcastFailed(reason): + return "Transaction broadcast failed: \(reason)" } } } From 7487f457fb495e5a2f472b31df803acd6c11f7df Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 31 Mar 2026 05:20:56 +0300 Subject: [PATCH 22/27] feat(swift-sdk): add InstantSend lock store and waitForInstantLock API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InstantLockStore captures IS lock bytes by txid from SPV events - WalletService.waitForInstantLock(txid:timeout:) async waits for IS lock - WalletService.broadcastTransaction() pass-through to SPV client - Completes step 3 of Coreβ†’Platform asset lock flow Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Services/WalletService.swift | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index ba19cdb52b..9c360f72c0 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -104,6 +104,9 @@ public class WalletService: ObservableObject { private var spvClient: SPVClient public private(set) var walletManager: CoreWalletManager + // InstantSend lock storage for asset lock flow + private let instantLockStore = InstantLockStore() + public init(modelContainer: ModelContainer, network: AppNetwork) { self.modelContainer = modelContainer self.network = network @@ -236,6 +239,19 @@ public class WalletService: ObservableObject { self.initializeNewSPVClient() } + // MARK: - Transaction Broadcasting + + /// Broadcast a raw transaction on the Core P2P network. + public func broadcastTransaction(_ transactionData: Data) throws { + try spvClient.broadcastTransaction(transactionData) + } + + /// Wait for an InstantSend lock for a specific transaction. + /// Returns the serialized IS lock bytes when received, or throws on timeout. + public func waitForInstantLock(txid: Data, timeout: TimeInterval = 30) async throws -> Data { + try await instantLockStore.waitForLock(txid: txid, timeout: timeout) + } + public func clearSpvStorage() { if syncProgress.state.isRunning() { print("[SPV][Clear] Sync task is running, cannot clear storage") @@ -314,7 +330,9 @@ public class WalletService: ObservableObject { func onBlocksProcessed(_ height: UInt32, _ hash: Data, _ newAddressCount: UInt32) {} func onMasternodeStateUpdated(_ height: UInt32) {} func onChainLockReceived(_ height: UInt32, _ hash: Data, _ signature: Data, _ validated: Bool) {} - func onInstantLockReceived(_ txid: Data, _ instantLockData: Data, _ validated: Bool) {} + func onInstantLockReceived(_ txid: Data, _ instantLockData: Data, _ validated: Bool) { + walletService.instantLockStore.store(txid: txid, lockData: instantLockData) + } func onSyncManagerError(_ manager: SPVSyncManager, _ errorMsg: String) { SDKLogger.error("Sync manager \(manager) error: \(errorMsg)") @@ -393,3 +411,74 @@ extension Data { return map { String(format: "%02hhx", $0) }.joined() } } + +// MARK: - InstantSend Lock Store + +/// Thread-safe store for InstantSend lock data, keyed by txid. +/// Supports async waiting for a specific txid's IS lock to arrive. +internal final class InstantLockStore: @unchecked Sendable { + private var locks: [Data: Data] = [:] + private var waiters: [Data: [(id: UUID, continuation: CheckedContinuation)]] = [:] + private let queue = DispatchQueue(label: "com.dash.instantlock-store") + + /// Store an IS lock. Resumes any waiters for this txid. + func store(txid: Data, lockData: Data) { + queue.sync { + locks[txid] = lockData + if let entries = waiters.removeValue(forKey: txid) { + for entry in entries { + entry.continuation.resume(returning: lockData) + } + } + } + } + + /// Wait for an IS lock for a specific txid. + /// Returns immediately if already cached, otherwise suspends until received or timeout. + func waitForLock(txid: Data, timeout: TimeInterval = 30) async throws -> Data { + // Check if already available + if let existing = queue.sync(execute: { locks[txid] }) { + return existing + } + + let waiterId = UUID() + + return try await withCheckedThrowingContinuation { continuation in + var alreadyResumed = false + + queue.sync { + // Double-check under lock + if let existing = locks[txid] { + continuation.resume(returning: existing) + alreadyResumed = true + return + } + waiters[txid, default: []].append((id: waiterId, continuation: continuation)) + } + + guard !alreadyResumed else { return } + + // Schedule timeout + queue.asyncAfter(deadline: .now() + timeout) { [weak self] in + guard let self else { return } + var timedOutContinuation: CheckedContinuation? + self.queue.sync { + if var list = self.waiters[txid] { + if let idx = list.firstIndex(where: { $0.id == waiterId }) { + timedOutContinuation = list[idx].continuation + list.remove(at: idx) + if list.isEmpty { + self.waiters.removeValue(forKey: txid) + } else { + self.waiters[txid] = list + } + } + } + } + timedOutContinuation?.resume(throwing: SPVError.transactionBroadcastFailed( + "InstantSend lock timeout after \(Int(timeout))s for txid \(txid.hexString)" + )) + } + } + } +} From 808c5f1187c59ac32f9856619d6d1dfca18f51df Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 31 Mar 2026 05:53:16 +0300 Subject: [PATCH 23/27] =?UTF-8?q?feat(swift-sdk):=20wire=20up=20Core?= =?UTF-8?q?=E2=86=92Platform=20asset=20lock=20transfer=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full end-to-end implementation: 1. Build asset lock tx via walletManager.buildAssetLockTransaction() 2. Broadcast on Core network via walletService.broadcastTransaction() 3. Wait for InstantSend lock via walletService.waitForInstantLock() 4. Submit to Platform via sdk.addresses.topUpAddressFromAssetLock() Also removes "Other Options / Asset Lock Coming Soon" section from Send view. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/ViewModels/SendViewModel.swift | 88 +++++++++++++++++-- .../Core/Views/SendTransactionView.swift | 1 + 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift index 438f1107a1..eaeb5253c2 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift @@ -1,4 +1,5 @@ import Foundation +import CommonCrypto import SwiftDashSDK /// Available send flow types based on source and destination. @@ -119,13 +120,19 @@ class SendViewModel: ObservableObject { func executeSend( sdk: SDK, shieldedService: ShieldedService, + walletService: WalletService, platformState: AppState, wallet: HDWallet ) async { guard let flow = detectedFlow, let amount = amount else { return } - guard let poolClient = shieldedService.poolClient else { - error = "Shielded pool not initialized" - return + + // Shielded flows need pool client; Core flows don't + let needsPoolClient = flow != .coreToPlatform && flow != .coreToCore + if needsPoolClient { + guard shieldedService.poolClient != nil else { + error = "Shielded pool not initialized" + return + } } isSending = true @@ -136,7 +143,7 @@ class SendViewModel: ObservableObject { do { switch flow { case .platformToShielded: - let bundle = try await poolClient.buildShieldBundle(amount: amount) + let bundle = try await shieldedService.poolClient!.buildShieldBundle(amount: amount) // Find an identity with sufficient balance to fund the shield guard let identity = platformState.identities.first(where: { $0.walletId == wallet.walletId && @@ -168,7 +175,7 @@ class SendViewModel: ObservableObject { case .shieldedToShielded: let parsed = DashAddress.parse(recipientAddress, network: network) guard case .orchard(let rawAddress) = parsed.type else { return } - let bundle = try await poolClient.buildTransferBundle( + let bundle = try await shieldedService.poolClient!.buildTransferBundle( recipientAddress: rawAddress, amount: amount ) @@ -181,7 +188,7 @@ class SendViewModel: ObservableObject { case .shieldedToPlatform: let parsed = DashAddress.parse(recipientAddress, network: network) guard case .platform(let addressBytes) = parsed.type else { return } - let bundle = try await poolClient.buildUnshieldBundle( + let bundle = try await shieldedService.poolClient!.buildUnshieldBundle( outputAddress: addressBytes, amount: amount ) @@ -195,7 +202,7 @@ class SendViewModel: ObservableObject { case .shieldedToCore: let parsed = DashAddress.parse(recipientAddress, network: network) guard case .core(let outputScript) = parsed.type else { return } - let bundle = try await poolClient.buildWithdrawalBundle( + let bundle = try await shieldedService.poolClient!.buildWithdrawalBundle( outputScript: outputScript, amount: amount, coreFeePerByte: 1, @@ -211,8 +218,42 @@ class SendViewModel: ObservableObject { successMessage = "Withdrawal submitted" case .coreToPlatform: - // TODO: Implement asset lock / Core β†’ Platform transfer - error = "Core to Platform transfer not yet implemented" + // Core β†’ Platform via asset lock + let parsed = DashAddress.parse(recipientAddress, network: network) + guard case .platform(let addressBytes) = parsed.type else { + error = "Invalid platform address" + return + } + + // 1. Build the asset lock transaction + let assetLockResult = try walletService.walletManager.buildAssetLockTransaction( + for: wallet, + creditOutputs: [(scriptPubKey: addressBytes, amount: amount)] + ) + + // 2. Broadcast on Core network + try walletService.broadcastTransaction(assetLockResult.transactionBytes) + + // Compute txid from transaction bytes (double SHA256, reversed) + let txid = computeTxid(from: assetLockResult.transactionBytes) + + // 3. Wait for InstantSend lock + let isLockData = try await walletService.waitForInstantLock(txid: txid, timeout: 30) + + // 4. Submit to Platform + let outPoint = buildOutPoint(txid: txid, outputIndex: assetLockResult.outputIndex) + _ = try sdk.addresses.topUpAddressFromAssetLock( + proofType: .instant, + instantLockData: isLockData, + transactionData: assetLockResult.transactionBytes, + outputIndex: assetLockResult.outputIndex, + coreChainLockedHeight: 0, + outPoint: outPoint, + assetLockPrivateKey: assetLockResult.privateKey, + outputs: [Addresses.AddressTransferOutput(addressBytes: addressBytes, amount: amount)] + ) + + successMessage = "Transfer to Platform complete" case .coreToCore: // TODO: Implement standard Core β†’ Core transaction @@ -226,4 +267,33 @@ class SendViewModel: ObservableObject { self.error = error.localizedDescription } } + + // MARK: - Helpers + + /// Compute txid from raw transaction bytes (double SHA256, reversed). + private func computeTxid(from txBytes: Data) -> Data { + var hash1 = Data(count: Int(CC_SHA256_DIGEST_LENGTH)) + var hash2 = Data(count: Int(CC_SHA256_DIGEST_LENGTH)) + txBytes.withUnsafeBytes { ptr in + hash1.withUnsafeMutableBytes { out in + _ = CC_SHA256(ptr.baseAddress, CC_LONG(txBytes.count), out.bindMemory(to: UInt8.self).baseAddress) + } + } + hash1.withUnsafeBytes { ptr in + hash2.withUnsafeMutableBytes { out in + _ = CC_SHA256(ptr.baseAddress, CC_LONG(hash1.count), out.bindMemory(to: UInt8.self).baseAddress) + } + } + // Txid is the reversed double-SHA256 + return Data(hash2.reversed()) + } + + /// Build a 36-byte OutPoint (txid + output index as little-endian u32). + private func buildOutPoint(txid: Data, outputIndex: UInt32) -> Data { + // OutPoint = txid (32 bytes, internal byte order) + index (4 bytes LE) + var outPoint = Data(txid.reversed()) // reversed back to internal order + var idx = outputIndex.littleEndian + outPoint.append(Data(bytes: &idx, count: 4)) + return outPoint + } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift index a6a17d0145..a8d3af8780 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift @@ -140,6 +140,7 @@ struct SendTransactionView: View { await viewModel.executeSend( sdk: sdk, shieldedService: shieldedService, + walletService: walletService, platformState: unifiedAppState.platformState, wallet: wallet ) From 3d47fac27258277a768a6dd0f110c50275a42b49 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 31 Mar 2026 05:59:44 +0300 Subject: [PATCH 24/27] fix(swift-sdk): convert platform address to P2PKH scriptPubKey for asset lock The asset lock credit outputs need Core scriptPubKeys (25 bytes), not raw platform address bytes (21 bytes). Convert type 0x00 (P2PKH) platform addresses to OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/ViewModels/SendViewModel.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift index eaeb5253c2..c2f0f583f7 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/ViewModels/SendViewModel.swift @@ -225,10 +225,24 @@ class SendViewModel: ObservableObject { return } + // Convert platform address (21 bytes: type + hash) to P2PKH scriptPubKey (25 bytes) + // Platform type 0x00 = P2PKH: OP_DUP OP_HASH160 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG + let creditScript: Data + if addressBytes.count == 21 && addressBytes[0] == 0x00 { + let pubkeyHash = addressBytes.dropFirst() // 20-byte hash + var script = Data([0x76, 0xa9, 0x14]) // OP_DUP OP_HASH160 PUSH20 + script.append(contentsOf: pubkeyHash) + script.append(contentsOf: [0x88, 0xac]) // OP_EQUALVERIFY OP_CHECKSIG + creditScript = script + } else { + // Pass through as-is for other address types + creditScript = addressBytes + } + // 1. Build the asset lock transaction let assetLockResult = try walletService.walletManager.buildAssetLockTransaction( for: wallet, - creditOutputs: [(scriptPubKey: addressBytes, amount: amount)] + creditOutputs: [(scriptPubKey: creditScript, amount: amount)] ) // 2. Broadcast on Core network From b224aff5a4d19d6553f26e48b7a16af07f21c4b9 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 31 Mar 2026 06:05:08 +0300 Subject: [PATCH 25/27] fix(swift-sdk): fix dangling pointer in asset lock FFI call Script pointers were captured inside withUnsafeBytes closures but used after the closure returned (dangling). Now all pointers are built inside a single withUnsafeBytes scope using a concatenated buffer. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Wallet/CoreWalletManager.swift | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index 129a0c62c9..7638b7f439 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -171,20 +171,13 @@ public class CoreWalletManager: ObservableObject { throw WalletError.walletError("At least one credit output required") } - // Prepare script pointers and lengths - var scriptPtrs: [UnsafePointer?] = [] - var scriptLens: [Int] = [] - var amounts: [UInt64] = [] - - // Keep Data alive for the duration of the FFI call - let scriptDatas = creditOutputs.map { $0.scriptPubKey } - - for (i, output) in creditOutputs.enumerated() { - scriptDatas[i].withUnsafeBytes { buffer in - scriptPtrs.append(buffer.baseAddress?.assumingMemoryBound(to: UInt8.self)) - } - scriptLens.append(output.scriptPubKey.count) - amounts.append(output.amount) + // Concatenate all scripts into a single contiguous buffer + // and build an array of pointers into it + var scriptLens: [Int] = creditOutputs.map { $0.scriptPubKey.count } + var amounts: [UInt64] = creditOutputs.map { $0.amount } + var concatenatedScripts = Data() + for output in creditOutputs { + concatenatedScripts.append(output.scriptPubKey) } var feeOut: UInt64 = 0 @@ -198,23 +191,36 @@ public class CoreWalletManager: ObservableObject { (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) var ffiError = FFIError() - let success = scriptPtrs.withUnsafeMutableBufferPointer { scriptPtrsBuffer in - scriptLens.withUnsafeMutableBufferPointer { scriptLensBuffer in - amounts.withUnsafeMutableBufferPointer { amountsBuffer in - wallet_build_and_sign_asset_lock_transaction( - sdkWalletManager.handle, - sdkWallet.handle, - accountIndex, - fundingType.rawValue, - identityIndex, - scriptPtrsBuffer.baseAddress, - scriptLensBuffer.baseAddress, - amountsBuffer.baseAddress, - count, - feePerKb, - &feeOut, - &txBytesOut, - &txLenOut, + // Build pointers inside withUnsafeBytes so they remain valid + let success = concatenatedScripts.withUnsafeBytes { allScriptsBuffer -> Bool in + guard let allScriptsBase = allScriptsBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return false + } + // Build array of pointers into the concatenated buffer + var scriptPtrs: [UnsafePointer?] = [] + var offset = 0 + for len in scriptLens { + scriptPtrs.append(allScriptsBase.advanced(by: offset)) + offset += len + } + + return scriptPtrs.withUnsafeMutableBufferPointer { scriptPtrsBuffer in + scriptLens.withUnsafeMutableBufferPointer { scriptLensBuffer in + amounts.withUnsafeMutableBufferPointer { amountsBuffer in + wallet_build_and_sign_asset_lock_transaction( + sdkWalletManager.handle, + sdkWallet.handle, + accountIndex, + fundingType.rawValue, + identityIndex, + scriptPtrsBuffer.baseAddress, + scriptLensBuffer.baseAddress, + amountsBuffer.baseAddress, + count, + feePerKb, + &feeOut, + &txBytesOut, + &txLenOut, &outputIndexOut, &privateKeyOut, &ffiError @@ -222,6 +228,7 @@ public class CoreWalletManager: ObservableObject { } } } + } guard success else { let msg = ffiError.message != nil ? String(cString: ffiError.message!) : "Unknown error" From 5b1c28f3aa3879e4be17cafd7ec9e2dbf1f5382c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 31 Mar 2026 06:43:05 +0300 Subject: [PATCH 26/27] chore: bump rust-dashcore to 6638745 (fix asset lock coin selection) Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 14 +++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6bcdc931d..61b367df3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1607,7 +1607,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=6638745c27119778d4c78959003955f00bad373c#6638745c27119778d4c78959003955f00bad373c" dependencies = [ "anyhow", "async-trait", @@ -1640,7 +1640,7 @@ dependencies = [ [[package]] name = "dash-spv-ffi" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=6638745c27119778d4c78959003955f00bad373c#6638745c27119778d4c78959003955f00bad373c" dependencies = [ "cbindgen 0.29.2", "clap", @@ -1665,7 +1665,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=6638745c27119778d4c78959003955f00bad373c#6638745c27119778d4c78959003955f00bad373c" dependencies = [ "anyhow", "base64-compat", @@ -1690,12 +1690,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=6638745c27119778d4c78959003955f00bad373c#6638745c27119778d4c78959003955f00bad373c" [[package]] name = "dashcore-rpc" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=6638745c27119778d4c78959003955f00bad373c#6638745c27119778d4c78959003955f00bad373c" dependencies = [ "dashcore-rpc-json", "hex", @@ -1708,7 +1708,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=6638745c27119778d4c78959003955f00bad373c#6638745c27119778d4c78959003955f00bad373c" dependencies = [ "bincode", "dashcore", @@ -1723,7 +1723,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=6638745c27119778d4c78959003955f00bad373c#6638745c27119778d4c78959003955f00bad373c" dependencies = [ "bincode", "dashcore-private", @@ -3829,7 +3829,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=6638745c27119778d4c78959003955f00bad373c#6638745c27119778d4c78959003955f00bad373c" dependencies = [ "aes", "async-trait", @@ -3857,7 +3857,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=6638745c27119778d4c78959003955f00bad373c#6638745c27119778d4c78959003955f00bad373c" dependencies = [ "cbindgen 0.29.2", "dashcore", @@ -3872,7 +3872,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a#5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" +source = "git+https://github.com/dashpay/rust-dashcore?rev=6638745c27119778d4c78959003955f00bad373c#6638745c27119778d4c78959003955f00bad373c" dependencies = [ "async-trait", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 2a27563d52..80a21c2046 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,13 +47,13 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } -dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "5db46b4d2bdc50b0fbc8d9acbebe72775bb4132a" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "6638745c27119778d4c78959003955f00bad373c" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "6638745c27119778d4c78959003955f00bad373c" } +dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "6638745c27119778d4c78959003955f00bad373c" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "6638745c27119778d4c78959003955f00bad373c" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "6638745c27119778d4c78959003955f00bad373c" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "6638745c27119778d4c78959003955f00bad373c" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "6638745c27119778d4c78959003955f00bad373c" } # Optimize heavy crypto crates even in dev/test builds so that # Halo 2 proof generation and verification run at near-release speed. From c3c9c7b03958906c8ec7c92e259f742a83589434 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 31 Mar 2026 10:08:15 +0300 Subject: [PATCH 27/27] fix(swift-sdk): replace continuation-based IS lock wait with simple polling The CheckedContinuation approach had race conditions between store() and timeout. Replaced with a simple poll loop (250ms interval) that checks the lock store until timeout. Simpler and crash-free. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/Services/WalletService.swift | 61 +++++-------------- 1 file changed, 16 insertions(+), 45 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index 9c360f72c0..aea6a23924 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -418,67 +418,38 @@ extension Data { /// Supports async waiting for a specific txid's IS lock to arrive. internal final class InstantLockStore: @unchecked Sendable { private var locks: [Data: Data] = [:] - private var waiters: [Data: [(id: UUID, continuation: CheckedContinuation)]] = [:] + private var continuations: [Data: CheckedContinuation] = [:] private let queue = DispatchQueue(label: "com.dash.instantlock-store") - /// Store an IS lock. Resumes any waiters for this txid. + /// Store an IS lock. Resumes waiter if one exists for this txid. func store(txid: Data, lockData: Data) { + var cont: CheckedContinuation? queue.sync { locks[txid] = lockData - if let entries = waiters.removeValue(forKey: txid) { - for entry in entries { - entry.continuation.resume(returning: lockData) - } - } + cont = continuations.removeValue(forKey: txid) } + cont?.resume(returning: lockData) } /// Wait for an IS lock for a specific txid. - /// Returns immediately if already cached, otherwise suspends until received or timeout. + /// Returns immediately if already cached, otherwise polls until received or timeout. func waitForLock(txid: Data, timeout: TimeInterval = 30) async throws -> Data { // Check if already available if let existing = queue.sync(execute: { locks[txid] }) { return existing } - let waiterId = UUID() - - return try await withCheckedThrowingContinuation { continuation in - var alreadyResumed = false - - queue.sync { - // Double-check under lock - if let existing = locks[txid] { - continuation.resume(returning: existing) - alreadyResumed = true - return - } - waiters[txid, default: []].append((id: waiterId, continuation: continuation)) - } - - guard !alreadyResumed else { return } - - // Schedule timeout - queue.asyncAfter(deadline: .now() + timeout) { [weak self] in - guard let self else { return } - var timedOutContinuation: CheckedContinuation? - self.queue.sync { - if var list = self.waiters[txid] { - if let idx = list.firstIndex(where: { $0.id == waiterId }) { - timedOutContinuation = list[idx].continuation - list.remove(at: idx) - if list.isEmpty { - self.waiters.removeValue(forKey: txid) - } else { - self.waiters[txid] = list - } - } - } - } - timedOutContinuation?.resume(throwing: SPVError.transactionBroadcastFailed( - "InstantSend lock timeout after \(Int(timeout))s for txid \(txid.hexString)" - )) + // Poll-based approach β€” simpler and avoids continuation resume races + let deadline = Date().addingTimeInterval(timeout) + while Date() < deadline { + try await Task.sleep(nanoseconds: 250_000_000) // 250ms + if let existing = queue.sync(execute: { locks[txid] }) { + return existing } } + + throw SPVError.transactionBroadcastFailed( + "InstantSend lock timeout after \(Int(timeout))s for txid \(txid.hexString)" + ) } }