Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -792,6 +792,37 @@ final class SwiftDashSDKContactsService: ObservableObject {
return missing
}

/// Identity ids whose DIP-15 pair Platform has confirmed enabled this
/// session. Platform never silently removes keys (they can only be
/// explicitly disabled), so one confirmation spares a network query on
/// every subsequent tab load.
private var platformConfirmedDashPayIdentities: Set<Data> = []

/// Authoritative Platform-side count of the missing DIP-15 keys. The
/// local SwiftData key rows lag when the pair was added from another
/// device — `missingDashPayKeyCount()` alone would keep showing the
/// "Enable DashPay" intro for an identity that is already enabled.
/// Returns nil when the query can't run (no SDK / no identity / network
/// error); callers keep the local answer then.
func missingDashPayKeyCountOnPlatform() async -> Int? {
guard let sdk = SwiftDashSDKHost.shared.sdk,
let ownerId = DWCurrentUserIdentityInfo.shared.identityId else {
return nil
}
if platformConfirmedDashPayIdentities.contains(ownerId) { return 0 }
do {
let keysById = try await sdk.identityGetKeys(identityId: ownerId.toBase58String())
let missing = DWIdentityKeyUpgrader.missingDashPayPurposes(inPlatformKeysById: keysById).count
if missing == 0 {
platformConfirmedDashPayIdentities.insert(ownerId)
}
return missing
Comment thread
QuantumExplorer marked this conversation as resolved.
Outdated
} catch {
Self.logger.error("👥 CONTACTS :: platform DashPay-key re-check failed: \(String(describing: error), privacy: .public)")
return nil
}
}

/// Estimated network fee for the enable-DashPay IdentityUpdate, in
/// duffs (1 duff = 1000 credits): the platform fee schedule's
/// `identity_update` minimum (100,000 credits) plus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,7 @@ enum DWIdentityKeyUpgrader {
// rows can lag — e.g. keys added from another device).
let keysById = try await sdk.identityGetKeys(identityId: ownerId.toBase58String())
let keys = keysById.values.compactMap { $0 as? [String: Any] }

func hasEnabledECDSAKey(purpose: Int) -> Bool {
keys.contains { key in
(key["purpose"] as? Int) == purpose
&& (key["type"] as? Int) == 0 // ECDSA_SECP256K1
&& (key["disabledAt"] == nil || key["disabledAt"] is NSNull)
}
}

var missingPurposes: [KeyPurpose] = []
if !hasEnabledECDSAKey(purpose: 1) { missingPurposes.append(.encryption) }
if !hasEnabledECDSAKey(purpose: 2) { missingPurposes.append(.decryption) }
let missingPurposes = Self.missingDashPayPurposes(inPlatformKeysById: keysById)
guard !missingPurposes.isEmpty else { return false }

Self.logger.info("🪪 KEY-UPGRADE :: identity missing \(missingPurposes.count, privacy: .public) DashPay key(s) — deriving + broadcasting IdentityUpdate")
Expand Down Expand Up @@ -100,6 +89,27 @@ enum DWIdentityKeyUpgrader {
return true
}

/// The DIP-15 purposes (ENCRYPTION / DECRYPTION) that lack an enabled
/// ECDSA_SECP256K1 key in a Platform `identityGetKeys` response. Shared
/// by the upgrade broadcast above and the Contacts tab's authoritative
/// "is Enable DashPay really needed?" re-check.
nonisolated static func missingDashPayPurposes(
inPlatformKeysById keysById: [String: Any]
) -> [KeyPurpose] {
let keys = keysById.values.compactMap { $0 as? [String: Any] }
func hasEnabledECDSAKey(purpose: Int) -> Bool {
keys.contains { key in
(key["purpose"] as? Int) == purpose
&& (key["type"] as? Int) == 0 // ECDSA_SECP256K1
&& (key["disabledAt"] == nil || key["disabledAt"] is NSNull)
}
}
var missing: [KeyPurpose] = []
if !hasEnabledECDSAKey(purpose: 1) { missing.append(.encryption) }
if !hasEnabledECDSAKey(purpose: 2) { missing.append(.decryption) }
return missing
}

private static func fetchIdentityRow(
ownerId: Data,
modelContainer: ModelContainer
Expand All @@ -111,3 +121,43 @@ enum DWIdentityKeyUpgrader {
return (try? context.fetch(descriptor))?.first
}
}

/// Re-fetches an identity from Platform through the Rust load pipeline
/// (`loadIdentity(atIndex:)`), folding the authoritative state — public
/// keys included — back into the wallet and, via the persistence event
/// channel, the local SwiftData rows. Backs the Storage Explorer's
/// pull-to-refresh so keys added from another device become visible.
///
/// dashpay target only (same as the upgrader above).
@MainActor
enum DWIdentityReloader {

private static let logger = Logger(
subsystem: "org.dashfoundation.dash",
category: "swift-sdk-migration.identity-reloader")

/// Reload the identity persisted at `identityIndex`. Errors are logged,
/// not thrown — pull-to-refresh has no failure UI, and the stale local
/// rows remain an honest fallback.
static func reload(identityIndex: UInt32) async {
guard let wallet = SwiftDashSDKHost.shared.wallet else { return }
do {
let id = try await wallet.loadIdentity(atIndex: identityIndex)
Self.logger.info("🪪 RELOAD :: identity at index \(identityIndex, privacy: .public) reloaded (found=\(id != nil, privacy: .public))")
} catch {
Self.logger.error("🪪 RELOAD :: identity reload failed at index \(identityIndex, privacy: .public): \(String(describing: error), privacy: .public)")
}
}

/// Reload the current user's main identity, resolving its identity
/// index from the persisted row. No-op when no identity is registered.
static func reloadCurrentUserIdentity() async {
guard let modelContainer = SwiftDashSDKHost.shared.modelContainer,
let ownerId = DWCurrentUserIdentityInfo.shared.identityId else { return }
var descriptor = FetchDescriptor<PersistentIdentity>(
predicate: #Predicate { $0.identityId == ownerId })
descriptor.fetchLimit = 1
guard let row = (try? modelContainer.mainContext.fetch(descriptor))?.first else { return }
await reload(identityIndex: row.identityIndex)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,33 @@ final class ContactsViewModel: ObservableObject {
service.refresh()
missingDashPayKeyCount = service.missingDashPayKeyCount()
needsDashPayEnable = missingDashPayKeyCount > 0
reconcileDashPayEnableWithPlatform()
let info = DWCurrentUserIdentityInfo.shared
ownDisplayName = info.displayName
ownUsername = info.username?.withoutDashSuffix
ownAvatarURL = info.avatarURL
ownIdentitySeed = info.identityId ?? Data()
}

/// Guards `reconcileDashPayEnableWithPlatform` against overlapping
/// checks when `refresh()` fires in bursts.
private var platformKeyCheckInFlight = false

/// The local key rows lag an "Enable DashPay" done on another device.
/// When they claim the DIP-15 pair is missing, confirm against
/// Platform's authoritative key set and correct the intro — most
/// importantly clearing it for an identity that is already enabled.
private func reconcileDashPayEnableWithPlatform() {
guard needsDashPayEnable, !platformKeyCheckInFlight else { return }
platformKeyCheckInFlight = true
Task {
defer { platformKeyCheckInFlight = false }
guard let missing = await service.missingDashPayKeyCountOnPlatform() else { return }
missingDashPayKeyCount = missing
needsDashPayEnable = missing > 0
}
}

/// Estimated IdentityUpdate fee for the confirm sheet, sized to the
/// keys actually missing: "~0.000131 DASH (≈ THB 0.13)" in the user's
/// local currency.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ struct PublicKeyStorageListView: View {
}
.navigationTitle("Public Keys (\(records.count))")
.overlay { if records.isEmpty { ContentUnavailableView("No Records", systemImage: "key") } }
#if DASHPAY
// Pull-to-refresh re-fetches the current user's identity from
// Platform so keys added on another device appear here.
.refreshable { await DWIdentityReloader.reloadCurrentUserIdentity() }
#endif
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ struct IdentityStorageDetailView: View {
}
.navigationTitle("Identity")
.navigationBarTitleDisplayMode(.inline)
#if DASHPAY
// Pull-to-refresh re-fetches this identity from Platform — the
// local rows (public keys especially) lag keys added on another
// device. @Query keeps the form live as the reload persists.
.refreshable { await DWIdentityReloader.reload(identityIndex: record.identityIndex) }
#endif
}
}

Expand Down
Loading