diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index c62681e1..5b4f299e 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ AE91CEED2C0FDB70000AAD20 /* SentAndReceivedValues+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE91CEEC2C0FDB70000AAD20 /* SentAndReceivedValues+Extensions.swift */; }; AE91CEEF2C0FDBC7000AAD20 /* CanonicalTx+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE91CEEE2C0FDBC7000AAD20 /* CanonicalTx+Extensions.swift */; }; AE96F6622A424C400055623C /* BDKSwiftExampleWalletReceiveViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE96F6612A424C400055623C /* BDKSwiftExampleWalletReceiveViewModelTests.swift */; }; + AE97E74D2E315A8F000A407D /* AddressType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE97E74C2E315A8F000A407D /* AddressType+Extensions.swift */; }; AEA0A6272E297203008A525B /* BitcoinDevKit in Frameworks */ = {isa = PBXBuildFile; productRef = AEA0A6262E297203008A525B /* BitcoinDevKit */; }; AEAB03112ABDDB86000C9528 /* FeeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEAB03102ABDDB86000C9528 /* FeeViewModel.swift */; }; AEAB03132ABDDBF4000C9528 /* AmountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEAB03122ABDDBF4000C9528 /* AmountViewModel.swift */; }; @@ -187,6 +188,7 @@ AE91CEEC2C0FDB70000AAD20 /* SentAndReceivedValues+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentAndReceivedValues+Extensions.swift"; sourceTree = ""; }; AE91CEEE2C0FDBC7000AAD20 /* CanonicalTx+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CanonicalTx+Extensions.swift"; sourceTree = ""; }; AE96F6612A424C400055623C /* BDKSwiftExampleWalletReceiveViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BDKSwiftExampleWalletReceiveViewModelTests.swift; sourceTree = ""; }; + AE97E74C2E315A8F000A407D /* AddressType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AddressType+Extensions.swift"; sourceTree = ""; }; AEAB03102ABDDB86000C9528 /* FeeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeViewModel.swift; sourceTree = ""; }; AEAB03122ABDDBF4000C9528 /* AmountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountViewModel.swift; sourceTree = ""; }; AEB130C82A44E4850087785B /* TransactionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailView.swift; sourceTree = ""; }; @@ -567,6 +569,7 @@ isa = PBXGroup; children = ( 77EDA65A2E2A5B3800A5E3AD /* URL+Extensions.swift */, + AE97E74C2E315A8F000A407D /* AddressType+Extensions.swift */, 77F0FDC82DA9A93700B30E4F /* Persister+Extensions.swift */, AEE6C74B2ABCB3E200442ADD /* Transaction+Extensions.swift */, AE83EFDA2C9D07B200B41244 /* ChainPosition+Extensions.swift */, @@ -762,6 +765,7 @@ AE783A012AB4E5E1005F0CBA /* BuildTransactionView.swift in Sources */, AE6F34DA2AA6C1E00087E700 /* Balance+Extensions.swift in Sources */, AED4CC0C2A1D3A9400CE1831 /* OnboardingView.swift in Sources */, + AE97E74D2E315A8F000A407D /* AddressType+Extensions.swift in Sources */, 77F0FDC92DA9A93D00B30E4F /* Persister+Extensions.swift in Sources */, AE6716012A9AC089005C193F /* KeyServiceError.swift in Sources */, 77AD9F062DBB031D00182E65 /* ActivityHomeHeaderView.swift in Sources */, diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/AddressType+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/AddressType+Extensions.swift new file mode 100644 index 00000000..2f735291 --- /dev/null +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/AddressType+Extensions.swift @@ -0,0 +1,35 @@ +// +// AddressType+Extensions.swift +// BDKSwiftExampleWallet +// +// Created by Matthew Ramsden on 7/23/25. +// + +import Foundation + +enum AddressType: String, CaseIterable { + case bip86 = "bip86" + case bip84 = "bip84" + + var description: String { + switch self { + case .bip86: return "bip86" + case .bip84: return "bip84" + } + } + + var displayName: String { + switch self { + case .bip86: return "BIP86 (Taproot)" + case .bip84: return "BIP84 (SegWit)" + } + } + + init?(stringValue: String) { + switch stringValue { + case "bip86": self = .bip86 + case "bip84": self = .bip84 + default: return nil + } + } +} diff --git a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings index 8729efd7..f1cc1fba 100644 --- a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings +++ b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings @@ -347,6 +347,9 @@ } } } + }, + "Address Type" : { + }, "Amount Error" : { "localizations" : { @@ -1003,6 +1006,9 @@ } } } + }, + "Select Address Type" : { + }, "Select Bitcoin Network" : { "localizations" : { @@ -1313,6 +1319,9 @@ } } } + }, + "Unknown" : { + }, "Unspent" : { "localizations" : { diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 1ab1bd5d..fee546af 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -56,6 +56,83 @@ private class BDKService { self.esploraClient = EsploraClient(url: self.esploraURL) } + private func getCurrentAddressType() -> AddressType { + let storedAddressTypeString = + try? keyClient.getAddressType() ?? AddressType.bip86.description + return AddressType(stringValue: storedAddressTypeString ?? "") ?? .bip86 + } + + private func createDescriptors( + for addressType: AddressType, + secretKey: DescriptorSecretKey, + network: Network + ) -> (descriptor: Descriptor, changeDescriptor: Descriptor) { + switch addressType { + case .bip86: + let descriptor = Descriptor.newBip86( + secretKey: secretKey, + keychainKind: .external, + network: network + ) + let changeDescriptor = Descriptor.newBip86( + secretKey: secretKey, + keychainKind: .internal, + network: network + ) + return (descriptor, changeDescriptor) + case .bip84: + let descriptor = Descriptor.newBip84( + secretKey: secretKey, + keychainKind: .external, + network: network + ) + let changeDescriptor = Descriptor.newBip84( + secretKey: secretKey, + keychainKind: .internal, + network: network + ) + return (descriptor, changeDescriptor) + } + } + + private func createPublicDescriptors( + for addressType: AddressType, + publicKey: DescriptorPublicKey, + fingerprint: String, + network: Network + ) -> (descriptor: Descriptor, changeDescriptor: Descriptor) { + switch addressType { + case .bip86: + let descriptor = Descriptor.newBip86Public( + publicKey: publicKey, + fingerprint: fingerprint, + keychainKind: .external, + network: network + ) + let changeDescriptor = Descriptor.newBip86Public( + publicKey: publicKey, + fingerprint: fingerprint, + keychainKind: .internal, + network: network + ) + return (descriptor, changeDescriptor) + case .bip84: + let descriptor = Descriptor.newBip84Public( + publicKey: publicKey, + fingerprint: fingerprint, + keychainKind: .external, + network: network + ) + let changeDescriptor = Descriptor.newBip84Public( + publicKey: publicKey, + fingerprint: fingerprint, + keychainKind: .internal, + network: network + ) + return (descriptor, changeDescriptor) + } + } + func getAddress() throws -> String { guard let wallet = self.wallet else { throw WalletError.walletNotFound @@ -117,16 +194,14 @@ private class BDKService { mnemonic: mnemonic, password: nil ) - let descriptor = Descriptor.newBip86( + let currentAddressType = getCurrentAddressType() + let descriptors = createDescriptors( + for: currentAddressType, secretKey: secretKey, - keychainKind: .external, - network: network - ) - let changeDescriptor = Descriptor.newBip86( - secretKey: secretKey, - keychainKind: .internal, network: network ) + let descriptor = descriptors.descriptor + let changeDescriptor = descriptors.changeDescriptor let backupInfo = BackupInfo( mnemonic: mnemonic.description, descriptor: descriptor.toStringWithSecret(), @@ -219,18 +294,15 @@ private class BDKService { let descriptorPublicKey = try DescriptorPublicKey.fromString(publicKey: xpubString) let fingerprint = descriptorPublicKey.masterFingerprint() - let descriptor = Descriptor.newBip86Public( - publicKey: descriptorPublicKey, - fingerprint: fingerprint, - keychainKind: .external, - network: network - ) - let changeDescriptor = Descriptor.newBip86Public( + let currentAddressType = getCurrentAddressType() + let descriptors = createPublicDescriptors( + for: currentAddressType, publicKey: descriptorPublicKey, fingerprint: fingerprint, - keychainKind: .internal, network: network ) + let descriptor = descriptors.descriptor + let changeDescriptor = descriptors.changeDescriptor let backupInfo = BackupInfo( mnemonic: "", @@ -454,6 +526,14 @@ extension BDKService { func setNeedsFullScan(_ value: Bool) { needsFullScan = value } + + func getAddressType() -> AddressType { + return getCurrentAddressType() + } + + func updateAddressType(_ newAddressType: AddressType) { + try? keyClient.saveAddressType(newAddressType.description) + } } struct BDKClient { @@ -481,6 +561,8 @@ struct BDKClient { let getEsploraURL: () -> String let updateNetwork: (Network) -> Void let updateEsploraURL: (String) -> Void + let getAddressType: () -> AddressType + let updateAddressType: (AddressType) -> Void } extension BDKClient { @@ -534,6 +616,12 @@ extension BDKClient { }, updateEsploraURL: { newURL in BDKService.shared.updateEsploraURL(newURL) + }, + getAddressType: { + BDKService.shared.getAddressType() + }, + updateAddressType: { newAddressType in + BDKService.shared.updateAddressType(newAddressType) } ) } @@ -591,7 +679,9 @@ extension BDKClient { getNetwork: { .signet }, getEsploraURL: { Constants.Config.EsploraServerURLNetwork.Signet.mutiny }, updateNetwork: { _ in }, - updateEsploraURL: { _ in } + updateEsploraURL: { _ in }, + getAddressType: { .bip86 }, + updateAddressType: { _ in } ) } #endif diff --git a/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift b/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift index 51714028..bbc61c5b 100644 --- a/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift +++ b/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift @@ -62,6 +62,14 @@ private struct KeyService { func saveNetwork(network: String) throws { keychain[string: "SelectedNetwork"] = network } + + func getAddressType() throws -> String? { + return keychain[string: "SelectedAddressType"] + } + + func saveAddressType(addressType: String) throws { + keychain[string: "SelectedAddressType"] = addressType + } } struct KeyClient { @@ -71,9 +79,11 @@ struct KeyClient { let getBackupInfo: () throws -> BackupInfo let getEsploraURL: () throws -> String? let getNetwork: () throws -> String? + let getAddressType: () throws -> String? let saveEsploraURL: (String) throws -> Void let saveBackupInfo: (BackupInfo) throws -> Void let saveNetwork: (String) throws -> Void + let saveAddressType: (String) throws -> Void private init( deleteBackupInfo: @escaping () throws -> Void, @@ -82,9 +92,11 @@ struct KeyClient { getBackupInfo: @escaping () throws -> BackupInfo, getEsploraURL: @escaping () throws -> String?, getNetwork: @escaping () throws -> String?, + getAddressType: @escaping () throws -> String?, saveBackupInfo: @escaping (BackupInfo) throws -> Void, saveEsploraURL: @escaping (String) throws -> Void, - saveNetwork: @escaping (String) throws -> Void + saveNetwork: @escaping (String) throws -> Void, + saveAddressType: @escaping (String) throws -> Void ) { self.deleteBackupInfo = deleteBackupInfo self.deleteEsplora = deleteEsplora @@ -92,9 +104,11 @@ struct KeyClient { self.getBackupInfo = getBackupInfo self.getEsploraURL = getEsploraURL self.getNetwork = getNetwork + self.getAddressType = getAddressType self.saveBackupInfo = saveBackupInfo self.saveEsploraURL = saveEsploraURL self.saveNetwork = saveNetwork + self.saveAddressType = saveAddressType } } @@ -106,9 +120,12 @@ extension KeyClient { getBackupInfo: { try KeyService().getBackupInfo() }, getEsploraURL: { try KeyService().getEsploraURL() }, getNetwork: { try KeyService().getNetwork() }, + getAddressType: { try KeyService().getAddressType() }, saveBackupInfo: { backupInfo in try KeyService().saveBackupInfo(backupInfo: backupInfo) }, saveEsploraURL: { url in try KeyService().saveEsploraURL(url: url) }, - saveNetwork: { network in try KeyService().saveNetwork(network: network) } + saveNetwork: { network in try KeyService().saveNetwork(network: network) }, + saveAddressType: { addressType in try KeyService().saveAddressType(addressType: addressType) + } ) } @@ -146,9 +163,11 @@ extension KeyClient { }, getEsploraURL: { nil }, getNetwork: { nil }, + getAddressType: { nil }, saveBackupInfo: { _ in }, saveEsploraURL: { _ in }, - saveNetwork: { _ in } + saveNetwork: { _ in }, + saveAddressType: { _ in } ) } #endif diff --git a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift index 49db4f6c..d1aa5bb9 100644 --- a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift @@ -38,6 +38,11 @@ class OnboardingViewModel: ObservableObject { bdkClient.updateEsploraURL(selectedURL) } } + @Published var selectedAddressType: AddressType = .bip86 { + didSet { + bdkClient.updateAddressType(selectedAddressType) + } + } @Published var words: String = "" var wordArray: [String] { if words.hasPrefix("xpub") || words.hasPrefix("tpub") || words.hasPrefix("vpub") { @@ -81,6 +86,7 @@ class OnboardingViewModel: ObservableObject { self.bdkClient = bdkClient self.selectedNetwork = bdkClient.getNetwork() self.selectedURL = bdkClient.getEsploraURL() + self.selectedAddressType = bdkClient.getAddressType() } func createWallet() { diff --git a/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift b/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift index 790fc87b..7446a082 100644 --- a/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift @@ -17,6 +17,7 @@ class SettingsViewModel: ObservableObject { @Published var esploraURL: String? @Published var inspectedScripts: UInt64 = 0 @Published var network: String? + @Published var addressType: AddressType? @Published var settingsError: AppError? @Published var showingSettingsViewErrorAlert = false @Published var walletSyncState: WalletSyncState = .notStarted @@ -37,6 +38,10 @@ class SettingsViewModel: ObservableObject { self.esploraURL = bdkClient.getEsploraURL() } + func getAddressType() { + self.addressType = bdkClient.getAddressType() + } + func delete() { do { try bdkClient.deleteWallet() diff --git a/BDKSwiftExampleWallet/View/OnboardingView.swift b/BDKSwiftExampleWallet/View/OnboardingView.swift index 62ac3a9e..77f71c85 100644 --- a/BDKSwiftExampleWallet/View/OnboardingView.swift +++ b/BDKSwiftExampleWallet/View/OnboardingView.swift @@ -140,6 +140,17 @@ struct OnboardingView: View { .tint(.primary) .opacity(animateContent ? 1 : 0) .animation(.easeOut(duration: 0.5).delay(1.5), value: animateContent) + + Picker("Address Type", selection: $viewModel.selectedAddressType) { + ForEach(AddressType.allCases, id: \.self) { type in + Text(type.displayName).tag(type) + } + } + .pickerStyle(.automatic) + .tint(.primary) + .accessibilityLabel("Select Address Type") + .opacity(animateContent ? 1 : 0) + .animation(.easeOut(duration: 0.5).delay(1.5), value: animateContent) } if !viewModel.words.isEmpty { diff --git a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift index df1aab97..e6fdb707 100644 --- a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift +++ b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift @@ -52,6 +52,19 @@ struct SettingsView: View { colorScheme == .light ? Color.gray.opacity(0.1) : Color.black.opacity(0.2) ) + Section(header: Text("Address Type")) { + if let addressType = viewModel.addressType { + Text(addressType.displayName) + .foregroundStyle(.primary) + } else { + Text("Unknown") + .foregroundStyle(.secondary) + } + } + .listRowBackground( + colorScheme == .light ? Color.gray.opacity(0.1) : Color.black.opacity(0.2) + ) + Section(header: Text("Wallet")) { Button { Task { @@ -105,6 +118,7 @@ struct SettingsView: View { .onAppear { viewModel.getNetwork() viewModel.getEsploraUrl() + viewModel.getAddressType() } .padding(.top, 40.0)