Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 1 addition & 1 deletion .platform-ref
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3122d378c240fa811af04b4aaf7972179a77359a
7853fef6a62f5f0c9fbee9802d3adbac9d74cded
26 changes: 24 additions & 2 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,7 @@
D9B1027C784C2F7ABE856C66 /* PaymentIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DE6B63FE6B9E24278B870E /* PaymentIntent.swift */; };
DA38F1C5CD32A52891D02D0E /* MayaConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7708BBFDD14AFF237FB72D71 /* MayaConstants.swift */; };
DA57A1010000000000000002 /* PastedAmountNormalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA57A1010000000000000001 /* PastedAmountNormalizationTests.swift */; };
A12280000000000000000002 /* WalletProtocolCompatibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12280000000000000000001 /* WalletProtocolCompatibilityTests.swift */; };
DA5A00000000000000000001 /* DWDashPayIdentityKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5A00000000000000000003 /* DWDashPayIdentityKeys.swift */; };
DA5A00000000000000000002 /* DWDashPayIdentityKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5A00000000000000000003 /* DWDashPayIdentityKeys.swift */; };
DA5A00000000000000000004 /* DashPayIdentityKeysTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5A00000000000000000005 /* DashPayIdentityKeysTests.swift */; };
Expand Down Expand Up @@ -2127,6 +2128,12 @@
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 */; };
24CAE0F59C6912E3100DD6D9 /* ProtocolCompatibilityBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2810B5A0391A0E9F8BA75A44 /* ProtocolCompatibilityBanner.swift */; };
37073F1701FD566FBB158D47 /* SwiftDashSDKProtocolCompatibilityClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817F5D3C43D9D0BACEAD27A0 /* SwiftDashSDKProtocolCompatibilityClient.swift */; };
5A6021A453D21DFFAC8AD456 /* WalletProtocolCompatibilityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608465F486B3F9801C82C850 /* WalletProtocolCompatibilityController.swift */; };
5AB9D51D588A37DCAE6685EB /* WalletProtocolCompatibilityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608465F486B3F9801C82C850 /* WalletProtocolCompatibilityController.swift */; };
62D8C3A8C3B76AB18193FF76 /* SwiftDashSDKProtocolCompatibilityClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817F5D3C43D9D0BACEAD27A0 /* SwiftDashSDKProtocolCompatibilityClient.swift */; };
EE4A14D13A5CF665A1E86BD0 /* ProtocolCompatibilityBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2810B5A0391A0E9F8BA75A44 /* ProtocolCompatibilityBanner.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -3896,6 +3903,7 @@
D9155233A128831C0ABF18A3 /* BIP70PaymentService+App.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "BIP70PaymentService+App.swift"; sourceTree = "<group>"; };
D99130732DE8F2E95F635F6B /* SwapConvertCardRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConvertCardRow.swift; sourceTree = "<group>"; };
DA57A1010000000000000001 /* PastedAmountNormalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastedAmountNormalizationTests.swift; sourceTree = "<group>"; };
A12280000000000000000001 /* WalletProtocolCompatibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletProtocolCompatibilityTests.swift; sourceTree = "<group>"; };
DA5A00000000000000000003 /* DWDashPayIdentityKeys.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DWDashPayIdentityKeys.swift; sourceTree = "<group>"; };
DA5A00000000000000000005 /* DashPayIdentityKeysTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashPayIdentityKeysTests.swift; sourceTree = "<group>"; };
DA8890030000000000000003 /* DWSecureAllocator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWSecureAllocator.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4011,6 +4019,9 @@
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; };
2810B5A0391A0E9F8BA75A44 /* ProtocolCompatibilityBanner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtocolCompatibilityBanner.swift; path = DashWallet/Sources/UI/Main/ProtocolCompatibilityBanner.swift; sourceTree = "<group>"; };
608465F486B3F9801C82C850 /* WalletProtocolCompatibilityController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WalletProtocolCompatibilityController.swift; path = DashWallet/Sources/Infrastructure/SwiftDashSDK/WalletProtocolCompatibilityController.swift; sourceTree = "<group>"; };
817F5D3C43D9D0BACEAD27A0 /* SwiftDashSDKProtocolCompatibilityClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwiftDashSDKProtocolCompatibilityClient.swift; path = DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKProtocolCompatibilityClient.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
Expand Down Expand Up @@ -7477,6 +7488,9 @@
75D5F3B5191EC270004AB296 = {
isa = PBXGroup;
children = (
608465F486B3F9801C82C850 /* WalletProtocolCompatibilityController.swift */,
817F5D3C43D9D0BACEAD27A0 /* SwiftDashSDKProtocolCompatibilityClient.swift */,
2810B5A0391A0E9F8BA75A44 /* ProtocolCompatibilityBanner.swift */,
C9D2C9552A320AC100D15901 /* DashPay */,
2ADF83FB23632D55008459A7 /* Shared */,
75D5F3C7191EC270004AB296 /* DashWallet */,
Expand Down Expand Up @@ -7648,6 +7662,7 @@
AA00100D2CA0B10001A0B10D /* SwapKitQuoteDecodingTests.swift */,
5EAD222130A0000000A1B202 /* DashAmountFormatterTests.swift */,
DA57A1010000000000000001 /* PastedAmountNormalizationTests.swift */,
A12280000000000000000001 /* WalletProtocolCompatibilityTests.swift */,
DA5A00000000000000000005 /* DashPayIdentityKeysTests.swift */,
D5E2D975669CB53883D56803 /* DWAvatarUploadClientTests.swift */,
);
Expand Down Expand Up @@ -10055,6 +10070,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5A6021A453D21DFFAC8AD456 /* WalletProtocolCompatibilityController.swift in Sources */,
37073F1701FD566FBB158D47 /* SwiftDashSDKProtocolCompatibilityClient.swift in Sources */,
24CAE0F59C6912E3100DD6D9 /* ProtocolCompatibilityBanner.swift in Sources */,
553696A24F315D361BC86231 /* VoteHistoryDAO.swift in Sources */,
25B84141680B6A9F225C995C /* BulkVoteSheet.swift in Sources */,
A1B2C3D40003VOTINGNODESEL /* VotingNodeSelectionSheet.swift in Sources */,
Expand Down Expand Up @@ -11001,6 +11019,7 @@
AA00100E2CA0B10001A0B10E /* SwapKitQuoteDecodingTests.swift in Sources */,
5EAD222230A0000000A1B202 /* DashAmountFormatterTests.swift in Sources */,
DA57A1010000000000000002 /* PastedAmountNormalizationTests.swift in Sources */,
A12280000000000000000002 /* WalletProtocolCompatibilityTests.swift in Sources */,
DA5A00000000000000000004 /* DashPayIdentityKeysTests.swift in Sources */,
215F9BB0CAADF4D47D95C444 /* DWAvatarUploadClientTests.swift in Sources */,
);
Expand Down Expand Up @@ -11040,6 +11059,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5AB9D51D588A37DCAE6685EB /* WalletProtocolCompatibilityController.swift in Sources */,
62D8C3A8C3B76AB18193FF76 /* SwiftDashSDKProtocolCompatibilityClient.swift in Sources */,
EE4A14D13A5CF665A1E86BD0 /* ProtocolCompatibilityBanner.swift in Sources */,
7062FCF792F292D84A0B819B /* VoteHistoryDAO.swift in Sources */,
63C3BBEF4EE77A6F02144B2D /* BulkVoteSheet.swift in Sources */,
A1B2C3D40002VOTINGNODESEL /* VotingNodeSelectionSheet.swift in Sources */,
Expand Down Expand Up @@ -13446,7 +13468,7 @@
OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -Xcc -fmodule-map-file=\"${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.modulemap\" -Xcc -fmodule-map-file=\"${PODS_CONFIGURATION_BUILD_DIR}/Moya/Moya.modulemap\" -Xcc -fmodule-map-file=\"${PODS_CONFIGURATION_BUILD_DIR}/SQLite.swift/SQLite.modulemap\" -Xcc -fmodule-map-file=\"${PODS_CONFIGURATION_BUILD_DIR}/SQLiteMigrationManager.swift/SQLiteMigrationManager.modulemap\" -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/FBLPromises/PromisesObjC.modulemap\" -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/SSZipArchive/SSZipArchive.modulemap\"";
PRODUCT_BUNDLE_IDENTIFIER = org.dashfoundation.dash;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "PIGGYCARDS_ENABLED DASHPAY $(DASH_DEVNET_FLAGS)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "PIGGYCARDS_ENABLED DASHPAY $(DASH_DEVNET_FLAGS) TESTFLIGHT";
SWIFT_OBJC_BRIDGING_HEADER = "DashWallet/dashwallet-Bridging-Header.h";
SWIFT_OBJC_INTERFACE_HEADER_NAME = "dashwallet-Swift.h";
SWIFT_VERSION = 5.0;
Expand Down Expand Up @@ -13638,7 +13660,7 @@
OTHER_SWIFT_FLAGS = "$(inherited) -DDASHPAY";
PRODUCT_BUNDLE_IDENTIFIER = org.dashfoundation.dash;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "PIGGYCARDS_ENABLED DASHPAY $(DASH_DEVNET_FLAGS)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "PIGGYCARDS_ENABLED DASHPAY $(DASH_DEVNET_FLAGS) TESTFLIGHT";
SWIFT_OBJC_BRIDGING_HEADER = "DashWallet/dashwallet-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,55 +19,38 @@

import Foundation

/// Funding asset locks we have already asked Platform about and been told are
/// already spent.
///
/// The SDK deliberately does NOT advance such a lock's status: the
/// already-consumed report arrives as a plain rejection from one DAPI node, not
/// as a quorum-authenticated proof, and stamping `Consumed` on an unverified
/// negative would permanently hide a lock that may still be recoverable
/// (`rs-platform-wallet` records it as "consumption unknown" instead).
///
/// That is the right call for the *status*, but it leaves the answer itself
/// nowhere: the coordinator's phase dies with the sheet, so the next launch
/// looks exactly like the first — "Completion unknown" and a live retry action
/// on every restored transfer. A wallet restored with a hundred shielded
/// transfers then shows a hundred permanently unfinished-looking rows.
///
/// This store records the weaker, honest fact — *we asked, and the network said
/// spent* — without claiming the transfer is proven complete. It is app-owned
/// and advisory: losing it costs one redundant probe, never money.
///
/// Superseded by a proved read of Platform's `SpentAssetLockTransactions` tree,
/// which would settle consumption authoritatively and let the status advance on
/// its own. Until that query exists client-side, this is the honest middle.
/// Wallet-scoped, unverified consumption reports for exact asset-lock outpoints.
/// Automatic recovery skips these reports; an explicit authenticated user action
/// can recheck the same lock. A report never proves success or authorizes a new lock.
final class AssetLockProbeStore {

static let shared = AssetLockProbeStore()

private let defaults = UserDefaults.standard
private let defaults: UserDefaults
private let lock = NSLock()
/// Scoped per wallet (`<legacyKey>_<activeWalletIdHex>`) exactly as
/// V1 reports have no output index and cannot safely be migrated to a
/// specific outpoint. Leave them unused until wallet cleanup; never turn
/// them into a report about every output in their transaction.
/// Scoped per wallet (`<storageKey>_<activeWalletIdHex>`) exactly as
/// `CoinJoinWithdrawalStore` is: an outpoint probed while wallet A was
/// active says nothing about wallet B. The bare key is the no-active-wallet
/// fallback.
private let legacyKey = "assetLockProbe.v1.alreadySpentTxids"
private let storageKey = "assetLockProbe.v2.alreadySpentOutpoints"
private var cacheKey: String?
private var cache: Set<Data>?

private init() {}

private func resolvedKey() -> String {
key(forWalletIdHex: WalletEnvironment.activeWalletIdHex as String?)
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}

/// The storage key for an EXPLICIT wallet. Callers that suspend before
/// writing must resolve their wallet up front and pass it here: the active
/// wallet can change mid-flight, and `resolvedKey()` would then file the
/// wallet can change mid-flight, and using the active wallet would then file the
/// result under whichever wallet happens to be active at write time.
private func key(forWalletIdHex walletIdHex: String?) -> String {
guard let walletIdHex, !walletIdHex.isEmpty else { return legacyKey }
return "\(legacyKey)_\(walletIdHex)"
guard let walletIdHex, !walletIdHex.isEmpty else { return storageKey }
return "\(storageKey)_\(walletIdHex)"
}

/// Hex id of the wallet a probe should be filed under, captured BEFORE the
Expand All @@ -85,30 +68,40 @@ final class AssetLockProbeStore {
return set
}

private func loaded() -> Set<Data> { loaded(key: resolvedKey()) }
private func outpointKey(_ txid: Data, vout: UInt32) -> Data {
var index = vout.littleEndian
return txid + withUnsafeBytes(of: &index) { Data($0) }
}

/// Record that `txid` (WIRE order) was probed and reported already spent.
/// Record that `(txid, vout)` (WIRE order) was reported already spent.
/// Idempotent and thread-safe.
/// `walletIdHex` is the wallet the probe belongs to, captured before the
/// network round-trip that produced it — passing it explicitly is what
/// keeps a wallet switch during that round-trip from filing the result
/// under the wrong wallet.
func record(txid: Data, walletIdHex: String?) {
func record(txid: Data, vout: UInt32, walletIdHex: String?) {
let outpoint = outpointKey(txid, vout: vout)
let key = key(forWalletIdHex: walletIdHex)
lock.lock(); defer { lock.unlock() }
var set = loaded(key: key)
guard !set.contains(txid) else { return }
set.insert(txid)
guard !set.contains(outpoint) else { return }
set.insert(outpoint)
cacheKey = key
cache = set
defaults.set(Array(set), forKey: key)
}

/// Whether `txid` (WIRE order, e.g. `Transaction.txHashData`) has already
/// been probed and answered "already spent". Thread-safe; cheap.
func contains(_ txid: Data) -> Bool {
/// Whether this exact outpoint (WIRE-order txid) was reported already spent.
/// Thread-safe; cheap. This report is not proof of consumption.
func contains(_ txid: Data, vout: UInt32) -> Bool {
contains(txid, vout: vout, walletIdHex: Self.currentWalletIdHex())
}

/// Explicit-wallet lookup for work that has already captured its runtime.
func contains(_ txid: Data, vout: UInt32, walletIdHex: String?) -> Bool {
let key = key(forWalletIdHex: walletIdHex)
lock.lock(); defer { lock.unlock() }
return loaded().contains(txid)
return loaded(key: key).contains(outpointKey(txid, vout: vout))
}

/// Clear a SINGLE wallet's probes (per-wallet Remove flow — the removed
Expand All @@ -117,8 +110,9 @@ final class AssetLockProbeStore {
func clearForWallet(walletIdHex: String) {
guard !walletIdHex.isEmpty else { return }
lock.lock(); defer { lock.unlock() }
let key = "\(legacyKey)_\(walletIdHex)"
let key = "\(storageKey)_\(walletIdHex)"
defaults.removeObject(forKey: key)
defaults.removeObject(forKey: "\(legacyKey)_\(walletIdHex)")
if cacheKey == key {
cacheKey = nil
cache = nil
Expand All @@ -131,7 +125,8 @@ final class AssetLockProbeStore {
func resetForWipe() {
lock.lock(); defer { lock.unlock() }
for key in defaults.dictionaryRepresentation().keys
where key == legacyKey || key.hasPrefix("\(legacyKey)_") {
where key == legacyKey || key.hasPrefix("\(legacyKey)_")
|| key == storageKey || key.hasPrefix("\(storageKey)_") {
defaults.removeObject(forKey: key)
}
cacheKey = nil
Expand Down
Loading
Loading