Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,6 @@ final class SwiftDashSDKContactsService: ObservableObject {
// refreshPaymentsProjection), so ride the snapshot refresh at
// most once a minute.
if Date().timeIntervalSince(lastPaymentsProjection) > 60 {
lastPaymentsProjection = Date()
refreshPaymentsProjection()
}
}
Expand All @@ -269,8 +268,14 @@ final class SwiftDashSDKContactsService: ObservableObject {
guard let manager = SwiftDashSDKHost.shared.manager,
let wallet = SwiftDashSDKHost.shared.wallet,
let ownerId = DWCurrentUserIdentityInfo.shared.identityId else {
// No identity yet: nothing was pulled, so leave the piggyback
// throttle unarmed. Arming it here spent the launch's first
// window on a call that returned immediately — the identity
// typically lands seconds later, and the next chance to project
// its payments was then a minute away.
return
}
lastPaymentsProjection = Date()
do {
let payments = try manager.refreshDashPayPayments(
walletId: wallet.walletId,
Expand Down Expand Up @@ -964,7 +969,7 @@ final class ContactsNotificationsBridge: NSObject {
final class DashPayPaymentTxLookup {
static let shared = DashPayPaymentTxLookup()

struct PaymentInfo: Sendable {
struct PaymentInfo: Sendable, Equatable {
let amountDuffs: UInt64
/// True when the wallet's identity SENT this payment.
let isOutgoing: Bool
Expand Down Expand Up @@ -1061,9 +1066,33 @@ final class DashPayPaymentTxLookup {
}
}

/// Swap the snapshot in, and say so when it actually changed.
///
/// The signal matters because nothing else carries it. The payment rows
/// behind this snapshot are written by an app-pulled projection, not by
/// the SDK persister, and they live in entities the transaction feed's
/// SwiftData-save filter ignores — so a feed already on screen kept
/// rendering rows with dash-spv's misread direction and no contact name
/// for the rest of the session. That was the whole of "DashPay
/// transactions only come back after a resync": the data was correct in
/// this cache, and nobody asked it again.
///
/// Gated on a real change: the projection re-runs on a timer, and an
/// unconditional post would rebuild the whole history list every pass.
private func store(_ map: [String: PaymentInfo]) {
lock.lock()
let changed = infoByTxid != map
infoByTxid = map
lock.unlock()

guard changed else { return }
NotificationCenter.default.post(name: Self.didChangeNotification, object: nil)
}
}

extension DashPayPaymentTxLookup {
/// Posted when the txid → DashPay-payment snapshot gained, lost, or
/// altered an entry. Consumers re-read `info(forTxidHex:)`.
static let didChangeNotification =
Notification.Name("DWDashPayPaymentTxLookupDidChange")
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ public final class PlatformAddressSyncCoordinator: NSObject, ObservableObject {
@Published public private(set) var isClearing: Bool = false
@Published public private(set) var lastSyncTime: Date? = nil
@Published public private(set) var lastError: String? = nil

/// Startup failure of `platformAddressWallet()`, held apart from
/// `lastError` because it outlives the events that clear it.
///
/// The address wallet and the manager's Platform address sync fail
/// independently: the sync can complete successfully while this wallet is
/// missing, and every success clears `lastError`. Publishing the startup
/// failure only once would let the first successful pass erase it, leaving
/// the status screen reporting a healthy sync over address surfaces that
/// cannot work. Kept here and re-published wherever `lastError` is cleared
/// on success. Cleared by `clearDisplay()`, which runs on teardown, wipe
/// and network-switch preparation — every path that invalidates the wallet
/// this error was recorded against — and overwritten by the next start.
private var addressWalletStartupError: String?
@Published private(set) var platformAccountAvailability: PlatformAccountAvailability = .unknown

@Published public private(set) var platformBalance: UInt64 = 0
Expand Down Expand Up @@ -301,7 +315,7 @@ public final class PlatformAddressSyncCoordinator: NSObject, ObservableObject {
try manager.startPlatformAddressSync()
}
isSyncing = true
lastError = nil
lastError = addressWalletStartupError
try await manager.syncPlatformAddressNow()
} catch {
isSyncing = false
Expand Down Expand Up @@ -700,6 +714,7 @@ public final class PlatformAddressSyncCoordinator: NSObject, ObservableObject {
lastSyncBlockTime = nil
lastSyncTime = nil
lastError = nil
addressWalletStartupError = nil
syncCountSinceLaunch = 0
totalTrunkQueries = 0
totalBranchQueries = 0
Expand Down Expand Up @@ -744,20 +759,31 @@ public final class PlatformAddressSyncCoordinator: NSObject, ObservableObject {
let accountAvailability = resolvePlatformAccountAvailability(
walletId: resolvedWallet.walletId)
let addressWallet: ManagedPlatformAddressWallet?
// Carried to the end rather than returned on: see below.
var addressWalletError: String?
do {
addressWallet = try resolvedWallet.platformAddressWallet()
} catch {
addressWallet = nil
if accountAvailability == .unavailable {
// A wallet can legitimately have Shielded state without a
// DIP-17 Platform Payment account. Keep the shared manager
// alive for Shielded/DashPay and expose a neutral UI state.
addressWallet = nil
Self.logger.info(
"🛰️ PLATFORM-ADDR :: no Platform Payment account; continuing without address wallet")
} else {
// Report the failure, but do not abort the start. Shielded,
// the DashPay sync loop and identity recovery share the
// manager, not the address wallet, and returning here took all
// three down with it: a restored wallet that hit this on the
// one start it gets per session lost its identity — and with
// it every contact and all contact payment history — until the
// app was relaunched. `addressWallet` is already an optional
// the rest of this method handles (the branch above sets it to
// nil and continues), so the only difference here is that
// `lastError` explains why the address surfaces are empty.
Self.logger.error("🛰️ PLATFORM-ADDR :: platformAddressWallet() failed: \(String(describing: error), privacy: .public)")
lastError = "platformAddressWallet failed: \(error.localizedDescription)"
return
addressWalletError = "platformAddressWallet failed: \(error.localizedDescription)"
}
}

Expand Down Expand Up @@ -823,7 +849,8 @@ public final class PlatformAddressSyncCoordinator: NSObject, ObservableObject {
self.platformAccountAvailability = accountAvailability
self.runningNetwork = network
self.isRunning = true
self.lastError = nil
self.addressWalletStartupError = addressWalletError
self.lastError = addressWalletError
Comment thread
coderabbitai[bot] marked this conversation as resolved.

subscribeToManager(manager: manager, walletId: resolvedWallet.walletId)
refreshDerivedAddresses()
Expand Down Expand Up @@ -1192,7 +1219,9 @@ public final class PlatformAddressSyncCoordinator: NSObject, ObservableObject {
guard let result = event.result(for: walletId) else { return }

if result.success {
lastError = nil
// A healthy address-sync pass says nothing about the address
// wallet, which failed to resolve at start and stays broken.
lastError = addressWalletStartupError
if result.checkpointHeight > 0 {
checkpointHeight = result.checkpointHeight
}
Expand Down
16 changes: 16 additions & 0 deletions DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2063,6 +2063,22 @@ extension HomeViewModel {
// sync loop (PlatformAddressSyncCoordinator).
}
.store(in: &cancellableBag)

// The reload above fires when the identity is adopted, which is before
// the DashPay sync loop has had a pass to fetch anything — so it runs
// against an empty payment lookup and was the only DashPay-aware
// trigger the feed had. The payments themselves land later, written by
// an app-pulled projection into entities `saveTouchesFeedRows` filters
// out, and are read through a computed property on rows that were
// already rendered. Without this the feed kept dash-spv's misread
// direction and a nameless contact for the rest of the session. The
// lookup posts only on a real change, so this is not a periodic reload.
NotificationCenter.default.publisher(for: DashPayPaymentTxLookup.didChangeNotification)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.txReloadRequests.send()
}
.store(in: &cancellableBag)
}
}
#endif
Loading