Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ba3a095
feat(ios): integrate protocol compatibility checks and recovery
llbartekll Oct 21, 2026
efbf952
refactor(ios): make protocol checks informational
llbartekll Oct 21, 2026
f2cc437
chore(ios): integrate develop with advisory protocol checks
llbartekll Oct 21, 2026
591c937
test(ios): cover local operation preparation invariants
llbartekll Oct 21, 2026
383634e
chore(ios): pin advisory SDK on v5.1-dev
llbartekll Oct 21, 2026
7d22d8d
fix(wallet): bind confirmations to the published wallet
llbartekll Oct 21, 2026
905d005
fix(wallet): align registration logs with current logger
llbartekll Oct 21, 2026
03a331f
merge: reconcile protocol advisory with restart-safe Core drain
llbartekll Oct 21, 2026
ba0c6fd
fix(wallet): address protocol review and integrate current develop
llbartekll Oct 22, 2026
8a3b326
fix(wallet): preserve settled funds after identity top-up refusal
llbartekll Oct 22, 2026
0c6d872
fix(wallet): observe protocol changes for the runtime lifetime
llbartekll Oct 22, 2026
fd9798e
chore(sdk): pin retry ambiguity fix
llbartekll Oct 22, 2026
8208167
chore(sdk): pin final retry observation revision
llbartekll Oct 22, 2026
55572a5
fix: retain committed asset lock across explicit send retries
llbartekll Oct 22, 2026
a45556e
fix: unwrap active network when capturing preparation
llbartekll Oct 22, 2026
54adfd1
fix: authorize explicit send retries of recorded asset locks
llbartekll Oct 22, 2026
d8b3147
Merge develop and pin reviewed protocol SDK
llbartekll Oct 22, 2026
3b103f6
fix: preserve existing transfer recovery and refresh stale confirmations
llbartekll Oct 22, 2026
26b932d
fix: integrate typed SDK unconfirmed submission errors
llbartekll Oct 22, 2026
c945c5f
chore(sdk): pin unconfirmed outcome presentation fix
llbartekll Oct 22, 2026
48d1014
fix: announce unconfirmed identity transfer outcomes
llbartekll Oct 22, 2026
0767e58
refactor(wallet): reduce protocol checks to a background update advisory
llbartekll Oct 23, 2026
4deec59
fix(wallet): match the existing Testflight Swift condition
llbartekll Oct 23, 2026
95ffa0d
chore(sdk): pin empty epoch result compatibility fix
llbartekll Oct 23, 2026
91860b3
fix(wallet): enable protocol auto-detection and unify update guidance
llbartekll Oct 27, 2026
c3f293d
docs: add protocol advisory review screenshots
llbartekll Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ jobs:
xcodebuild -version
swift --version

- name: Test protocol observation and update guidance
run: |
python3 scripts/test_protocol_compatibility.py
python3 scripts/test_protocol_compatibility.py --testflight

# Same key as release-dashpay-testflight.yml so the two workflows share
# one cached artifact per platform commit.
- name: Restore release XCFramework cache
Expand Down
2 changes: 1 addition & 1 deletion .platform-ref
Original file line number Diff line number Diff line change
@@ -1 +1 @@
84851397d7ad43988a5ad2c3860ca9e9e88677e3
5b89fe8b529c66d5e972880196545c902285258b
22 changes: 22 additions & 0 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2127,6 +2127,13 @@
A00CC68AE2499E4496BAECC6 /* UsernameRecoveryUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5C4244CC6C1E4DE4A7B0786 /* UsernameRecoveryUITests.swift */; };
21360CD4E679B2CFEBA61EC0 /* SettingsRowsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4105B7F65847F4A633063BD0 /* SettingsRowsUITests.swift */; };
0CC58E06B79948481E52B517 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2ABCA9172357A61B00092C09 /* Foundation.framework */; };
104ED77DD2298A025814CF83 /* SwiftDashSDKProtocolCompatibilityClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4994DE8F1D303A26F3CA8F48 /* SwiftDashSDKProtocolCompatibilityClient.swift */; };
1FED152A2C6BCE50AFB0445B /* WalletProtocolCompatibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE98C99944BC32EA668EBF52 /* WalletProtocolCompatibilityTests.swift */; };
3825BD7459C4884DD70D9A90 /* WalletProtocolCompatibilityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465C77403526106317A41475 /* WalletProtocolCompatibilityController.swift */; };
A976B61C558151C1AC300A54 /* ProtocolCompatibilityBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28E4AA75ADC1A7BAAA6C5E7 /* ProtocolCompatibilityBanner.swift */; };
C25AD4D28CA0532D65F80780 /* ProtocolCompatibilityBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28E4AA75ADC1A7BAAA6C5E7 /* ProtocolCompatibilityBanner.swift */; };
E64A1C72095A9DD989E8379B /* SwiftDashSDKProtocolCompatibilityClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4994DE8F1D303A26F3CA8F48 /* SwiftDashSDKProtocolCompatibilityClient.swift */; };
E97F36169DF0F8814359881A /* WalletProtocolCompatibilityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465C77403526106317A41475 /* WalletProtocolCompatibilityController.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -4011,6 +4018,10 @@
C5C4244CC6C1E4DE4A7B0786 /* UsernameRecoveryUITests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameRecoveryUITests.swift; sourceTree = "<group>"; };
4105B7F65847F4A633063BD0 /* SettingsRowsUITests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsRowsUITests.swift; sourceTree = "<group>"; };
222014736AFDB62525FA244F /* DashPayRecoveryUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DashPayRecoveryUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
465C77403526106317A41475 /* WalletProtocolCompatibilityController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WalletProtocolCompatibilityController.swift; path = DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletProtocolCompatibilityController.swift; sourceTree = "<group>"; };
4994DE8F1D303A26F3CA8F48 /* SwiftDashSDKProtocolCompatibilityClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwiftDashSDKProtocolCompatibilityClient.swift; path = DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKProtocolCompatibilityClient.swift; sourceTree = "<group>"; };
DE98C99944BC32EA668EBF52 /* WalletProtocolCompatibilityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WalletProtocolCompatibilityTests.swift; path = DashWalletTests/WalletProtocolCompatibilityTests.swift; sourceTree = "<group>"; };
E28E4AA75ADC1A7BAAA6C5E7 /* ProtocolCompatibilityBanner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtocolCompatibilityBanner.swift; path = DashWallet/Sources/UI/Main/ProtocolCompatibilityBanner.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
Expand Down Expand Up @@ -7477,6 +7488,10 @@
75D5F3B5191EC270004AB296 = {
isa = PBXGroup;
children = (
465C77403526106317A41475 /* WalletProtocolCompatibilityController.swift */,
4994DE8F1D303A26F3CA8F48 /* SwiftDashSDKProtocolCompatibilityClient.swift */,
E28E4AA75ADC1A7BAAA6C5E7 /* ProtocolCompatibilityBanner.swift */,
DE98C99944BC32EA668EBF52 /* WalletProtocolCompatibilityTests.swift */,
C9D2C9552A320AC100D15901 /* DashPay */,
2ADF83FB23632D55008459A7 /* Shared */,
75D5F3C7191EC270004AB296 /* DashWallet */,
Expand Down Expand Up @@ -10055,6 +10070,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3825BD7459C4884DD70D9A90 /* WalletProtocolCompatibilityController.swift in Sources */,
E64A1C72095A9DD989E8379B /* SwiftDashSDKProtocolCompatibilityClient.swift in Sources */,
A976B61C558151C1AC300A54 /* ProtocolCompatibilityBanner.swift in Sources */,
553696A24F315D361BC86231 /* VoteHistoryDAO.swift in Sources */,
25B84141680B6A9F225C995C /* BulkVoteSheet.swift in Sources */,
A1B2C3D40003VOTINGNODESEL /* VotingNodeSelectionSheet.swift in Sources */,
Expand Down Expand Up @@ -10944,6 +10962,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1FED152A2C6BCE50AFB0445B /* WalletProtocolCompatibilityTests.swift in Sources */,
2A2120EC22145CAE009906DC /* DWAmountInputValidatorTests.m in Sources */,
17E47391430C11AC728B69A3 /* DWRegistrationPhaseAdapterTests.swift in Sources */,
F44028CBF49F4A2FE6268874 /* UsernameRegistrationRecoveryTests.swift in Sources */,
Expand Down Expand Up @@ -11040,6 +11059,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E97F36169DF0F8814359881A /* WalletProtocolCompatibilityController.swift in Sources */,
104ED77DD2298A025814CF83 /* SwiftDashSDKProtocolCompatibilityClient.swift in Sources */,
C25AD4D28CA0532D65F80780 /* ProtocolCompatibilityBanner.swift in Sources */,
7062FCF792F292D84A0B819B /* VoteHistoryDAO.swift in Sources */,
63C3BBEF4EE77A6F02144B2D /* BulkVoteSheet.swift in Sources */,
A1B2C3D40002VOTINGNODESEL /* VotingNodeSelectionSheet.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import Foundation
import OSLog
import SwiftData
import SwiftDashSDK
import UIKit

enum MnemonicFirstWalletCreationError: Error {
case mnemonicPersistence(Error)
Expand Down Expand Up @@ -339,6 +340,10 @@ final class SwiftDashSDKHost {

// MARK: - Owned state

let protocolCompatibility = WalletProtocolCompatibilityController(
foregroundNotification: UIApplication.willEnterForegroundNotification,
log: { DWLogger.log($0) })

private(set) var sdk: SDK?
private(set) var manager: PlatformWalletManager?
private(set) var wallet: ManagedPlatformWallet?
Expand Down Expand Up @@ -1284,6 +1289,7 @@ final class SwiftDashSDKHost {
/// invariant doesn't hold here.
@discardableResult
func stopAsync() async -> PlatformWalletShutdownMetrics? {
protocolCompatibility.detach()
let drainWatch = contactCryptoDrainWatch
contactCryptoDrainWatch = nil
drainWatch?.cancel()
Expand Down Expand Up @@ -2028,6 +2034,9 @@ final class SwiftDashSDKHost {
modelContainer = handles.modelContainer
runningNetwork = handles.network
ContestedUsernameFee.shared.runtimeDidStart()
protocolCompatibility.attach(
client: SwiftDashSDKProtocolCompatibilityClient(sdk: handles.sdk),
network: String(describing: handles.network))
}

// MARK: - ModelContainer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import SwiftDashSDK

@MainActor
struct SwiftDashSDKProtocolCompatibilityClient: WalletProtocolCompatibilityClient {
let sdk: SDK

func queryEpoch() async throws {
// The historical epoch's proof carries fresh signed response metadata.
// getEpochsInfo dispatches blocking FFI on the SDK's query queue.
_ = try await sdk.getEpochsInfo(startEpoch: 0, count: 1, ascending: true)
Comment thread
llbartekll marked this conversation as resolved.
Resolved
}

func protocolVersionInfo() throws -> ProtocolVersionInfo {
try sdk.protocolVersionInfo()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Combine
import Foundation
import SwiftDashSDK

@MainActor
protocol WalletProtocolCompatibilityClient {
func queryEpoch() async throws
func protocolVersionInfo() throws -> ProtocolVersionInfo
}

/// An advisory observation owned by the host. No operation depends on this task.
@MainActor
final class WalletProtocolCompatibilityController: ObservableObject {
@Published private(set) var snapshot: ProtocolVersionInfo?
private(set) var runtimeID: UUID?
private var client: (any WalletProtocolCompatibilityClient)?
private var network: String?
private var pending: Task<Void, Never>?
private var foregroundSubscription: AnyCancellable?
private let log: (String) -> Void

init(
foregroundNotification: Notification.Name,
notificationCenter: NotificationCenter = .default,
log: @escaping (String) -> Void = { _ in }
) {
self.log = log
// Only the host-supplied willEnterForeground event. Dismissing Face ID
// emits didBecomeActive, which must not start another network query.
foregroundSubscription = notificationCenter.publisher(for: foregroundNotification)
.sink { [weak self] _ in
Task { @MainActor [weak self] in self?.refresh() }
}
}

@discardableResult
func attach(client: any WalletProtocolCompatibilityClient, network: String) -> Task<Void, Never> {
detach()
runtimeID = UUID()
self.client = client
self.network = network
return refresh()
}

func detach() {
pending?.cancel()
pending = nil
client = nil
runtimeID = nil
network = nil
snapshot = nil
}

@discardableResult
func refresh() -> Task<Void, Never> {
if let pending { return pending }
guard let client, let runtimeID, let network else { return Task {} }
let task = Task { [weak self] in
let started = Date()
var outcome = "success"
do { try await client.queryEpoch() }
catch { outcome = "query failed: \(error)" }
// The SDK may retain a verified observation even if this query
// failed. An error must not erase an earlier warning.
let observation = Result { try client.protocolVersionInfo() }
guard let self, self.runtimeID == runtimeID else { return }
self.pending = nil
switch observation {
case .success(let info): self.snapshot = info
case .failure(let error): outcome += "; snapshot failed: \(error)"
}
self.log("Protocol version network=\(network) runtime=\(runtimeID) elapsed=\(Date().timeIntervalSince(started)) outcome=\(outcome) info=\(String(describing: self.snapshot))")
}
pending = task
return task
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ struct SDKIdentityProfileSheet: View {
ScrollView {
VStack(alignment: .leading, spacing: 24) {
header
ProtocolCompatibilityBanner(controller: SwiftDashSDKHost.shared.protocolCompatibility)
Divider()
infoSection
if identityIsLoading {
Expand Down
3 changes: 3 additions & 0 deletions DashWallet/Sources/UI/Home/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@ struct HomeViewContent<Content: View>: View {
.frame(height: viewModel.headerHeight)
}

ProtocolCompatibilityBanner(controller: SwiftDashSDKHost.shared.protocolCompatibility)
.padding(.horizontal, 16)

SyncingHeaderView(onFilterTap: {
showFilterDialog = true
}, onSyncTap: {
Expand Down
63 changes: 63 additions & 0 deletions DashWallet/Sources/UI/Main/ProtocolCompatibilityBanner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Combine
import SwiftUI
import SwiftDashSDK

@MainActor
final class ProtocolCompatibilityBannerViewModel: ObservableObject {
@Published private(set) var snapshot: ProtocolVersionInfo?
private var subscription: AnyCancellable?

init(controller: WalletProtocolCompatibilityController) {
snapshot = controller.snapshot
subscription = controller.$snapshot.sink { [weak self] in self?.snapshot = $0 }
}

var message: String? {
guard snapshot?.newerProtocolAvailable == true else { return nil }
Comment thread
llbartekll marked this conversation as resolved.
Resolved
#if Testflight
Comment thread
llbartekll marked this conversation as resolved.
Resolved
return NSLocalizedString("The network uses a newer protocol than this version of the app. Check for updates in TestFlight.", comment: "Protocol version advisory")
#else
return NSLocalizedString("The network uses a newer protocol than this version of the app. Check for available updates.", comment: "Protocol version advisory")
#endif
}

var updateURL: URL? {
#if Testflight
return nil
#else
return URL(string: "https://apps.apple.com/app/dash-wallet/id1206647026")
Comment thread
llbartekll marked this conversation as resolved.
Resolved
#endif
}

}

struct ProtocolCompatibilityBanner: View {
@StateObject private var model: ProtocolCompatibilityBannerViewModel
@Environment(\.openURL) private var openURL

init(controller: WalletProtocolCompatibilityController) {
_model = StateObject(wrappedValue: ProtocolCompatibilityBannerViewModel(controller: controller))
}

var body: some View {
Group {
if let message = model.message {
VStack(alignment: .leading, spacing: 8) {
Text(message)
.font(.footnote)
.fixedSize(horizontal: false, vertical: true)
if let url = model.updateURL {
Button(NSLocalizedString("Check for update", comment: "Protocol version update")) {
openURL(url)
}
}
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
.accessibilityElement(children: .contain)
}
}
.fixedSize(horizontal: false, vertical: true)
}
}
Loading
Loading