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
2 changes: 1 addition & 1 deletion DASHSYNC_MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Status meanings:
| 12 | Network switch | Done; teardown tail | Remove the temporary DashSync wallet-registry mirror with C6-E. |
| 13 | Backup seed phrase | Done | Reads the active SDK wallet mnemonic from host-owned storage. |
| 14 | Wipe wallet | Done; teardown tail | Remove the DashSync registry/Core Data wipe arm after all consumers are gone. |
| 15 | Provider-key derivation | Done for retained scope | Owner/Voting are SDK-native; Operator/HPMN and legacy “used at” UI were intentionally dropped because the required public-key/list lookup surfaces are unavailable. |
| 15 | Provider-key derivation | Done | All four families are SDK-native: Owner/Voting via the key-wallet path surface, Operator (BLS) and Evonode Operator (Ed25519) via `ManagedPlatformWallet.providerKeyAtIndex` (`platform_wallet_provider_key_at_index` grew per-index BLS/EdDSA public-key export, removing the earlier blocker). Overview counts and per-keypair “used at” restored from the Rust masternode aggregation (`PlatformWalletManager.masternodes(for:)`). |
| 16 | DashPay identity creation | Done; invitation tail | Standard SDK-funded identity/name registration is migrated, with three funding paths (Core asset-lock, Platform Payment, shielded Type-20 — the privacy-preserving default, gated by `ShieldedIdentityFundingReadiness`). Invitation-funded create/accept remains part of the invitation decision. |
| 17 | DashPay identity/profile read-write | Done; compatibility tail | Retire remaining `DSBlockchainIdentity`-typed profile properties/categories and route every old view directly through `DWCurrentUserIdentityInfo` / `DWProfileUpdateBridge`. |
| 18 | DashPay contacts and pay-to-contact | Done | PR #787 rebuilt contacts on SwiftDashSDK/SwiftData/SwiftUI and removed the old contacts subsystem. Invitations were explicitly out of scope. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ class DerivationPathKeysViewModel: ObservableObject {
func item(for info: DerivationPathInfo, at index: Int) -> DerivationPathKeysItem {
model.itemForInfo(info, atIndex: index)
}

func usageInfo(at index: Int) -> String {
model.usageInfoForKey(at: index)
}
}

// MARK: - DerivationPathKeysContentView
Expand Down Expand Up @@ -75,12 +79,18 @@ struct DerivationPathKeysContentView: View {
// Keypair sections
ForEach(0..<viewModel.numberOfSections, id: \.self) { sectionIndex in
VStack(alignment: .leading, spacing: 16) {
// Section header: "Keypair N"
Text(NSLocalizedString("Keypair", comment: "") + " \(sectionIndex)")
.font(.callout.weight(.semibold))
.foregroundColor(Color(uiColor: .dw_label()))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
// Section header: "Keypair N" + usage line
VStack(alignment: .leading, spacing: 2) {
Text(NSLocalizedString("Keypair", comment: "") + " \(sectionIndex)")
.font(.callout.weight(.semibold))
.foregroundColor(Color(uiColor: .dw_label()))

Text(viewModel.usageInfo(at: sectionIndex))
.font(.caption)
.foregroundColor(Color(uiColor: .dw_secondaryText()))
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)

// Keys card
VStack(spacing: kMenuVGap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ enum DerivationPathInfo {
case address
case privateKey
case wifPrivateKey
case publicKey
case publicKeyLegacy
case platformNodeId
case tenderdashNodeKey
}

extension DerivationPathInfo {
Expand All @@ -52,6 +56,14 @@ extension DerivationPathInfo {
return NSLocalizedString("Private key", comment: "")
case .wifPrivateKey:
return NSLocalizedString("WIF Private key", comment: "")
case .publicKey:
return NSLocalizedString("Public key", comment: "")
case .publicKeyLegacy:
return NSLocalizedString("Public key (legacy)", comment: "")
case .platformNodeId:
return NSLocalizedString("Platform Node ID", comment: "")
case .tenderdashNodeKey:
return NSLocalizedString("Tenderdash node key (base64)", comment: "")
}
}
}
Expand All @@ -63,6 +75,10 @@ extension MNKey {
return [.address, .privateKey, .wifPrivateKey]
case .voting:
return [.address, .privateKey, .wifPrivateKey]
case .operator:
return [.publicKey, .publicKeyLegacy, .privateKey]
case .evonodeOperator:
return [.platformNodeId, .publicKey, .privateKey, .tenderdashNodeKey]
}
}
}
Expand All @@ -75,14 +91,25 @@ final class DerivationPathKeysModel {

let infoItems: [DerivationPathInfo]

var visibleIndexes: Int = 0
var visibleIndexes: Int

private let deriver: MasternodeProviderKeyDeriver?
private let usage: MasternodeKeyUsage
private let ecdsaDeriver: MasternodeProviderKeyDeriver?
private let providerDeriver: ProviderKeyDeriver?

init(key: MNKey) {
self.key = key
infoItems = key.infos
deriver = MasternodeProviderKeyDeriver(key: key)
usage = MasternodeKeyUsage.resolve()
visibleIndexes = usage.firstUnusedIndex(for: key)
switch key {
case .owner, .voting:
ecdsaDeriver = MasternodeProviderKeyDeriver(key: key)
providerDeriver = nil
case .operator, .evonodeOperator:
ecdsaDeriver = nil
providerDeriver = ProviderKeyDeriver(key: key)
}
}

func showNextKey() {
Expand All @@ -104,18 +131,45 @@ extension DerivationPathKeysModel {
infoItems.count
}

func usageInfoForKey(at index: Int) -> String {
guard let use = usage.use(for: key, at: index) else {
return NSLocalizedString("Not yet used", comment: "")
}
guard let service = use.serviceAddress, !service.isEmpty else {
return NSLocalizedString("Used", comment: "")
}
if use.revoked {
return NSLocalizedString("Previously used at: ", comment: "") + service
}
return NSLocalizedString("Used at: ", comment: "") + service
}

func itemForInfo(_ info: DerivationPathInfo, atIndex index: Int) -> DerivationPathKeysItem {
let unavailable = NSLocalizedString("Not available", comment: "")
let value: String
let index = UInt32(index)
let value: String?
switch info {
case .address:
value = deriver?.address(at: UInt32(index)) ?? unavailable
value = ecdsaDeriver?.address(at: index)
case .privateKey:
value = deriver?.privateKeyHex(at: UInt32(index)) ?? unavailable
switch key {
case .owner, .voting:
value = ecdsaDeriver?.privateKeyHex(at: index)
case .operator, .evonodeOperator:
value = providerDeriver?.key(at: index)?.privateKeyHex
}
case .wifPrivateKey:
value = deriver?.wif(at: UInt32(index)) ?? unavailable
value = ecdsaDeriver?.wif(at: index)
case .publicKey:
value = providerDeriver?.key(at: index)?.publicKeyHex
case .publicKeyLegacy:
value = providerDeriver?.key(at: index)?.legacyPublicKeyHex
case .platformNodeId:
value = providerDeriver?.key(at: index)?.nodeIdHex
case .tenderdashNodeKey:
value = providerDeriver?.tenderdashNodeKeyBase64(at: index)
}
return DerivationPathKeysItem(info: info, value: value)
return DerivationPathKeysItem(info: info, value: value ?? unavailable)
}
}

Expand All @@ -128,10 +182,10 @@ extension DerivationPathKeysModel {
/// voting `m/9'/<coin>'/3'/1'`, owner `m/9'/<coin>'/3'/2'` (ECDSA, fully
/// hardened account path, soft key index; coin = 5' mainnet / 1' testnet).
///
/// Only Owner/Voting are supported — Operator (BLS) and HPMN/Platform (EdDSA)
/// were removed because the FFI doesn't export their per-index public keys.
/// Internal (not private): `MasternodeKeyUsage` reuses `address(at:)` for
/// its owner/voting address join.
@MainActor
private final class MasternodeProviderKeyDeriver {
final class MasternodeProviderKeyDeriver {
private let key: MNKey
private let masterPath: String
private let accountType: AccountType
Expand All @@ -154,6 +208,10 @@ private final class MasternodeProviderKeyDeriver {
case .owner:
path = "m/9'/\(coinType)/3'/2'"
type = .providerOwnerKeys
case .operator, .evonodeOperator:
// BLS / Ed25519 families derive through `ProviderKeyDeriver`
// (the platform-wallet FFI), not the key-wallet path surface.
return nil
}

guard let (manager, wallet, walletId) = SwiftDashSDKHost.shared.derivationWallet() else {
Expand Down Expand Up @@ -193,6 +251,8 @@ private final class MasternodeProviderKeyDeriver {
account = collection.getProviderVotingKeysAccount()
case .owner:
account = collection.getProviderOwnerKeysAccount()
case .operator, .evonodeOperator:
account = nil
}

guard let pool = account?.getAddressPool(type: .single) ?? account?.getExternalAddressPool(),
Expand All @@ -202,3 +262,65 @@ private final class MasternodeProviderKeyDeriver {
return info.address
}
}

// MARK: - ProviderKeyDeriver

/// Derives masternode Operator (BLS) and Evonode Operator (Ed25519
/// platform-node) keys through the platform-wallet FFI
/// (`ManagedPlatformWallet.providerKeyAtIndex`). All derivation and
/// serialization (modern + legacy BLS encodings, the platform node id)
/// happens on the Rust side; results are memoized per index because the
/// Ed25519 family pulls the wallet seed through the mnemonic resolver on
/// every call.
@MainActor
private final class ProviderKeyDeriver {
private let kind: ManagedPlatformWallet.ProviderKeyKind
private let wallet: ManagedPlatformWallet
private var cache: [UInt32: ManagedPlatformWallet.ProviderDerivedKey] = [:]

init?(key: MNKey) {
switch key {
case .operator:
kind = .operatorBLS
case .evonodeOperator:
kind = .platformNodeEdDSA
case .owner, .voting:
return nil
}
guard let wallet = SwiftDashSDKHost.shared.wallet else {
return nil
}
self.wallet = wallet
}

func key(at index: UInt32) -> ManagedPlatformWallet.ProviderDerivedKey? {
if let cached = cache[index] {
return cached
}
// The screen is auth-gated and shows private-key rows for every
// family, so derive with the private scalar included up front.
guard let derived = try? wallet.providerKeyAtIndex(
kind: kind,
index: index,
includePrivate: true
) else {
return nil
}
cache[index] = derived
return derived
}

/// The Ed25519 platform-node key in dashmate's "Enter Ed25519 node key"
/// format: base64 of the 64-byte `priv(32) ‖ pub(32)` concatenation.
/// Pure re-encoding of the two hex strings the FFI returned — no crypto.
func tenderdashNodeKeyBase64(at index: UInt32) -> String? {
guard kind == .platformNodeEdDSA,
let derived = key(at: index),
let privateHex = derived.privateKeyHex,
let priv = Data(hex: privateHex), priv.count == 32,
let pub = Data(hex: derived.publicKeyHex), pub.count == 32 else {
return nil
}
return (priv + pub).base64EncodedString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ struct KeysOverviewContentView: View {

// Keys list container
VStack(spacing: kMenuVGap) {
ForEach(viewModel.items, id: \.self) { item in
ForEach(viewModel.items) { item in
KeyItemButton(item: item) {
handleItemTap(item)
handleItemTap(item.key)
}
}
}
Expand Down Expand Up @@ -79,18 +79,29 @@ struct KeysOverviewContentView: View {
// MARK: - KeyItemButton

struct KeyItemButton: View {
let item: MNKey
let item: KeysOverviewViewModel.Item
let action: () -> Void

var body: some View {
Button(action: action) {
HStack(spacing: 0) {
Text(item.title)
.font(.subheadline)
.foregroundColor(Color(uiColor: .dw_label()))
VStack(alignment: .leading, spacing: 2) {
Text(item.key.title)
.font(.subheadline)
.foregroundColor(Color(uiColor: .dw_label()))

Text(String.localizedStringWithFormat(NSLocalizedString("%d key(s)", comment: "#bc-ignore!"), item.keyCount))
.font(.caption)
.foregroundColor(Color(uiColor: .dw_secondaryText()))
}

Spacer()

Text(String.localizedStringWithFormat(NSLocalizedString("%ld used", comment: "#bc-ignore!"), item.usedCount))
.font(.footnote)
.foregroundColor(Color(uiColor: .dw_secondaryText()))
.padding(.trailing, 8)

Image("list-chevron-right")
.resizable()
.aspectRatio(contentMode: .fit)
Expand All @@ -109,14 +120,28 @@ struct KeyItemButton: View {

// MARK: - KeysOverviewViewModel

@MainActor
class KeysOverviewViewModel: ObservableObject {
@Published var items: [MNKey] = MNKey.allCases
}
struct Item: Identifiable {
let key: MNKey
/// Keys to show: everything up to the first unused index, minimum 1
/// (DashSync's `max(firstUnusedIndex, 1)` semantics).
let keyCount: Int
let usedCount: Int

var id: MNKey { key }
}

// MARK: - MNKey Identifiable
@Published var items: [Item] = []

extension MNKey: Identifiable {
var id: Self { self }
init() {
let usage = MasternodeKeyUsage.resolve()
items = MNKey.allCases.map { key in
Item(key: key,
keyCount: max(usage.firstUnusedIndex(for: key), 1),
usedCount: usage.usedCount(for: key))
}
}
}

// MARK: - Preview
Expand Down
Loading
Loading