From 4891ddff4947814de9762012be7b95c4297249ee Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 9 Nov 2024 15:05:30 -0600 Subject: [PATCH 1/9] wip: import and show and save descriptor --- .../Service/BDK Service/BDKService.swift | 58 ++++++++++++++++++- .../View Model/OnboardingViewModel.swift | 11 +++- .../View/OnboardingView.swift | 24 +++++--- 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 29fb2fde..f82d93a3 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -148,6 +148,53 @@ private class BDKService { self.wallet = wallet } + func createWallet(descriptor: String?) throws { + let documentsDirectoryURL = URL.documentsDirectory + let walletDataDirectoryURL = documentsDirectoryURL.appendingPathComponent("wallet_data") + + if FileManager.default.fileExists(atPath: walletDataDirectoryURL.path) { + try FileManager.default.removeItem(at: walletDataDirectoryURL) + } else { + } + + let baseUrl = + try keyClient.getEsploraURL() ?? Constants.Config.EsploraServerURLNetwork.Signet.mutiny + + guard let descriptorString = descriptor, !descriptorString.isEmpty else { + throw WalletError.walletNotFound + } + + let cleanDescriptor = + descriptorString.split(separator: "#").first.map(String.init) ?? descriptorString + let descriptor = try Descriptor(descriptor: cleanDescriptor, network: network) + let changeDescriptorString = cleanDescriptor.replacingOccurrences(of: "/0/*", with: "/1/*") + let changeDescriptor = try Descriptor(descriptor: changeDescriptorString, network: network) + + let backupInfo = BackupInfo( + mnemonic: "", + descriptor: descriptor.toStringWithSecret(), + changeDescriptor: changeDescriptor.toStringWithSecret() + ) + + try keyClient.saveBackupInfo(backupInfo) + try keyClient.saveNetwork(self.network.description) + try keyClient.saveEsploraURL(baseUrl) + + try FileManager.default.ensureDirectoryExists(at: walletDataDirectoryURL) + try FileManager.default.removeOldFlatFileIfNeeded(at: documentsDirectoryURL) + let persistenceBackendPath = walletDataDirectoryURL.appendingPathComponent("wallet.sqlite") + .path + let connection = try Connection(path: persistenceBackendPath) + self.connection = connection + let wallet = try Wallet( + descriptor: descriptor, + changeDescriptor: changeDescriptor, + network: network, + connection: connection + ) + self.wallet = wallet + } + private func loadWallet(descriptor: Descriptor, changeDescriptor: Descriptor) throws { let documentsDirectoryURL = URL.documentsDirectory let walletDataDirectoryURL = documentsDirectoryURL.appendingPathComponent("wallet_data") @@ -313,7 +360,8 @@ extension BDKService { struct BDKClient { let loadWallet: () throws -> Void let deleteWallet: () throws -> Void - let createWallet: (String?) throws -> Void + let createWalletFromSeed: (String?) throws -> Void + let createWalletFromDescriptor: (String?) throws -> Void let getBalance: () throws -> Balance let transactions: () throws -> [CanonicalTx] let listUnspent: () throws -> [LocalOutput] @@ -338,7 +386,10 @@ extension BDKClient { static let live = Self( loadWallet: { try BDKService.shared.loadWalletFromBackup() }, deleteWallet: { try BDKService.shared.deleteWallet() }, - createWallet: { words in try BDKService.shared.createWallet(words: words) }, + createWalletFromSeed: { words in try BDKService.shared.createWallet(words: words) }, + createWalletFromDescriptor: { descriptor in + try BDKService.shared.createWallet(descriptor: descriptor) + }, getBalance: { try BDKService.shared.getBalance() }, transactions: { try BDKService.shared.transactions() }, listUnspent: { try BDKService.shared.listUnspent() }, @@ -387,7 +438,8 @@ extension BDKClient { static let mock = Self( loadWallet: {}, deleteWallet: {}, - createWallet: { _ in }, + createWalletFromSeed: { _ in }, + createWalletFromDescriptor: { _ in }, getBalance: { .mock }, transactions: { return [ diff --git a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift index 56b7a706..5c527c27 100644 --- a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift @@ -15,6 +15,11 @@ import SwiftUI class OnboardingViewModel: ObservableObject { let bdkClient: BDKClient + var isDescriptor: Bool { + words.hasPrefix("tr(") || words.hasPrefix("wpkh(") || words.hasPrefix("wsh(") + || words.hasPrefix("sh(") + } + @AppStorage("isOnboarding") var isOnboarding: Bool? @Published var createWithPersistError: CreateWithPersistError? @Published var networkColor = Color.gray @@ -72,7 +77,11 @@ class OnboardingViewModel: ObservableObject { func createWallet() { do { - try bdkClient.createWallet(words) + if isDescriptor { + try bdkClient.createWalletFromDescriptor(words) + } else { + try bdkClient.createWalletFromSeed(words) + } DispatchQueue.main.async { self.isOnboarding = false } diff --git a/BDKSwiftExampleWallet/View/OnboardingView.swift b/BDKSwiftExampleWallet/View/OnboardingView.swift index 09965740..d857e24d 100644 --- a/BDKSwiftExampleWallet/View/OnboardingView.swift +++ b/BDKSwiftExampleWallet/View/OnboardingView.swift @@ -109,14 +109,22 @@ struct OnboardingView: View { .tint(.primary) if viewModel.wordArray != [] { - SeedPhraseView( - words: viewModel.wordArray, - preferredWordsPerRow: 2, - usePaging: true, - wordsPerPage: 4 - ) - .frame(height: 200) - .padding() + if viewModel.isDescriptor { + Text(viewModel.words) + .font(.system(.caption, design: .monospaced)) + .lineLimit(1) + .truncationMode(.middle) + .padding() + } else { + SeedPhraseView( + words: viewModel.wordArray, + preferredWordsPerRow: 2, + usePaging: true, + wordsPerPage: 4 + ) + .frame(height: 200) + .padding() + } } Spacer() From 796d859bce85a5c17515a1e97ac9d1bbb0ef9c70 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 9 Nov 2024 15:19:11 -0600 Subject: [PATCH 2/9] wip: update seedview to show descriptor --- .../Resources/Localizable.xcstrings | 8 ++ .../View/Settings/SeedView.swift | 95 +++++++++++-------- .../View/Settings/SettingsView.swift | 4 +- 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings index ee4207f3..c85c2604 100644 --- a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings +++ b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings @@ -320,6 +320,7 @@ } }, "Delete Seed" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -328,6 +329,9 @@ } } } + }, + "Delete Wallet" : { + }, "Descriptors" : { @@ -640,6 +644,7 @@ } }, "Show Seed" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -648,6 +653,9 @@ } } } + }, + "Show Wallet" : { + }, "Showing Seed Error" : { "localizations" : { diff --git a/BDKSwiftExampleWallet/View/Settings/SeedView.swift b/BDKSwiftExampleWallet/View/Settings/SeedView.swift index 6c3e6efa..93e97585 100644 --- a/BDKSwiftExampleWallet/View/Settings/SeedView.swift +++ b/BDKSwiftExampleWallet/View/Settings/SeedView.swift @@ -24,54 +24,65 @@ struct SeedView: View { let publicDescriptor = viewModel.publicDescriptor, let publicChangeDescriptor = viewModel.publicChangeDescriptor { + if backupInfo.mnemonic.isEmpty { + Text(backupInfo.descriptor) + .font(.system(.caption, design: .monospaced)) + .lineLimit(1) + .truncationMode(.middle) + .padding() + } else { + SeedPhraseView( + words: backupInfo.mnemonic.components(separatedBy: " "), + preferredWordsPerRow: 2, + usePaging: true, + wordsPerPage: 4 + ) + } - SeedPhraseView( - words: backupInfo.mnemonic.components(separatedBy: " "), - preferredWordsPerRow: 2, - usePaging: true, - wordsPerPage: 4 - ) - - VStack { - Text("Seed is not synced across devices.") - Text("Please make sure to write it down and store it securely.") + if !backupInfo.mnemonic.isEmpty { + VStack { + Text("Seed is not synced across devices.") + Text("Please make sure to write it down and store it securely.") + } + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding() } - .font(.caption) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .padding() - HStack { - Spacer() - Button { - UIPasteboard.general.string = backupInfo.mnemonic - isCopied = true - showCheckmark = true - DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { - isCopied = false - showCheckmark = false + if !backupInfo.mnemonic.isEmpty { + HStack { + Spacer() + Button { + UIPasteboard.general.string = backupInfo.mnemonic + isCopied = true + showCheckmark = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { + isCopied = false + showCheckmark = false + } + } label: { + HStack { + Image( + systemName: showCheckmark + ? "document.on.document.fill" : "document.on.document" + ) + .contentTransition(.symbolEffect(.replace)) + Text("Seed") + .bold() + } } - } label: { - HStack { - Image( - systemName: showCheckmark - ? "document.on.document.fill" : "document.on.document" + .buttonStyle( + BitcoinFilled( + width: 120, + height: 40, + tintColor: .primary, + textColor: Color(uiColor: .systemBackground), + isCapsule: true ) - .contentTransition(.symbolEffect(.replace)) - Text("Seed") - .bold() - } - } - .buttonStyle( - BitcoinFilled( - width: 120, - height: 40, - tintColor: .primary, - textColor: Color(uiColor: .systemBackground), - isCapsule: true ) - ) - Spacer() + Spacer() + } } HStack { diff --git a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift index 5fb8b45b..c5d6a77c 100644 --- a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift +++ b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift @@ -73,7 +73,7 @@ struct SettingsView: View { Button { showingShowSeedConfirmation = true } label: { - Text(String(localized: "Show Seed")) + Text(String(localized: "Show Wallet")) .foregroundStyle(.red) } } @@ -86,7 +86,7 @@ struct SettingsView: View { showingDeleteSeedConfirmation = true } label: { HStack { - Text(String(localized: "Delete Seed")) + Text(String(localized: "Delete Wallet")) .foregroundStyle(.red) } } From 7c334e2426f94dd6d241970c71174d555cc76583 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 9 Nov 2024 17:34:34 -0600 Subject: [PATCH 3/9] rename seedview --- .../project.pbxproj | 16 +++++++-------- ...el.swift => WalletRecoveryViewModel.swift} | 20 +++++++++---------- .../View/Settings/SettingsView.swift | 2 +- ...eedView.swift => WalletRecoveryView.swift} | 14 ++++++------- 4 files changed, 26 insertions(+), 26 deletions(-) rename BDKSwiftExampleWallet/View Model/Settings/{SeedViewModel.swift => WalletRecoveryViewModel.swift} (68%) rename BDKSwiftExampleWallet/View/Settings/{SeedView.swift => WalletRecoveryView.swift} (92%) diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index 16a72c7a..6e396fa7 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -34,8 +34,8 @@ AE2B8C1D2A9678C900815B2F /* FeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE2B8C1C2A9678C900815B2F /* FeeService.swift */; }; AE2B8C1F2A96797300815B2F /* RecommendedFees.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE2B8C1E2A96797300815B2F /* RecommendedFees.swift */; }; AE2F255D2BED0BFB002A9AC6 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE2F255C2BED0BFB002A9AC6 /* AppError.swift */; }; - AE34DDAC2B6B31ED00F04AD4 /* SeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAB2B6B31ED00F04AD4 /* SeedView.swift */; }; - AE34DDAE2B6B320F00F04AD4 /* SeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAD2B6B320F00F04AD4 /* SeedViewModel.swift */; }; + AE34DDAC2B6B31ED00F04AD4 /* WalletRecoveryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAB2B6B31ED00F04AD4 /* WalletRecoveryView.swift */; }; + AE34DDAE2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAD2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift */; }; AE3646262BEDB01200B04E25 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3646252BEDB01200B04E25 /* FileManager+Extensions.swift */; }; AE3902A42A3B4CD900BEC318 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3902A32A3B4CD900BEC318 /* HomeView.swift */; }; AE49847C2A1BBBD6009951E2 /* BDKSwiftExampleWalletApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE49847B2A1BBBD6009951E2 /* BDKSwiftExampleWalletApp.swift */; }; @@ -126,8 +126,8 @@ AE2B8C1C2A9678C900815B2F /* FeeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeService.swift; sourceTree = ""; }; AE2B8C1E2A96797300815B2F /* RecommendedFees.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedFees.swift; sourceTree = ""; }; AE2F255C2BED0BFB002A9AC6 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; - AE34DDAB2B6B31ED00F04AD4 /* SeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedView.swift; sourceTree = ""; }; - AE34DDAD2B6B320F00F04AD4 /* SeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedViewModel.swift; sourceTree = ""; }; + AE34DDAB2B6B31ED00F04AD4 /* WalletRecoveryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletRecoveryView.swift; sourceTree = ""; }; + AE34DDAD2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletRecoveryViewModel.swift; sourceTree = ""; }; AE3646252BEDB01200B04E25 /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = ""; }; AE3902A32A3B4CD900BEC318 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; AE4984782A1BBBD6009951E2 /* BDKSwiftExampleWallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BDKSwiftExampleWallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -303,7 +303,7 @@ isa = PBXGroup; children = ( AE2ADD732B61E8F500C2A823 /* SettingsView.swift */, - AE34DDAB2B6B31ED00F04AD4 /* SeedView.swift */, + AE34DDAB2B6B31ED00F04AD4 /* WalletRecoveryView.swift */, ); path = Settings; sourceTree = ""; @@ -359,7 +359,7 @@ isa = PBXGroup; children = ( AE2ADD772B61EFFE00C2A823 /* SettingsViewModel.swift */, - AE34DDAD2B6B320F00F04AD4 /* SeedViewModel.swift */, + AE34DDAD2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift */, ); path = Settings; sourceTree = ""; @@ -665,7 +665,7 @@ AEB130C92A44E4850087785B /* TransactionDetailView.swift in Sources */, AE287E772C0F6D200036A748 /* Array+Extensions.swift in Sources */, AE6715FD2A9AC056005C193F /* PriceServiceError.swift in Sources */, - AE34DDAC2B6B31ED00F04AD4 /* SeedView.swift in Sources */, + AE34DDAC2B6B31ED00F04AD4 /* WalletRecoveryView.swift in Sources */, AE2ADD742B61E8F500C2A823 /* SettingsView.swift in Sources */, AE2381AF2C605B1D00F6B00C /* ActivityListViewModel.swift in Sources */, AE6F34D82AA6C1800087E700 /* Network+Extensions.swift in Sources */, @@ -689,7 +689,7 @@ AE2381B32C60877600F6B00C /* LocalOutputListView.swift in Sources */, AE783A052AB4F51F005F0CBA /* String+Extensions.swift in Sources */, AE29ED112BBE318A00EB9C4F /* TransactionItemView.swift in Sources */, - AE34DDAE2B6B320F00F04AD4 /* SeedViewModel.swift in Sources */, + AE34DDAE2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift in Sources */, AE0C30F92A804B65008F1EAE /* OnboardingViewModel.swift in Sources */, AE3902A42A3B4CD900BEC318 /* HomeView.swift in Sources */, AE0C30FD2A804BC1008F1EAE /* ReceiveViewModel.swift in Sources */, diff --git a/BDKSwiftExampleWallet/View Model/Settings/SeedViewModel.swift b/BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift similarity index 68% rename from BDKSwiftExampleWallet/View Model/Settings/SeedViewModel.swift rename to BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift index baa5523a..0d613d2b 100644 --- a/BDKSwiftExampleWallet/View Model/Settings/SeedViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift @@ -1,5 +1,5 @@ // -// SeedViewModel.swift +// WalletRecoveryViewModel.swift // BDKSwiftExampleWallet // // Created by Matthew Ramsden on 1/31/24. @@ -11,25 +11,25 @@ import SwiftUI @Observable @MainActor -class SeedViewModel { +class WalletRecoveryViewModel { let bdkClient: BDKClient var backupInfo: BackupInfo? var publicDescriptor: Descriptor? var publicChangeDescriptor: Descriptor? - var seedViewError: AppError? - var showingSeedViewErrorAlert: Bool + var walletRecoveryViewError: AppError? + var showingWalletRecoveryViewErrorAlert: Bool init( bdkClient: BDKClient = .live, backupInfo: BackupInfo? = nil, - seedViewError: AppError? = nil, - showingSeedViewErrorAlert: Bool = false + walletRecoveryViewError: AppError? = nil, + showingWalletRecoveryViewErrorAlert: Bool = false ) { self.bdkClient = bdkClient self.backupInfo = backupInfo - self.seedViewError = seedViewError - self.showingSeedViewErrorAlert = showingSeedViewErrorAlert + self.walletRecoveryViewError = walletRecoveryViewError + self.showingWalletRecoveryViewErrorAlert = showingWalletRecoveryViewErrorAlert } func getNetwork() -> Network { @@ -55,8 +55,8 @@ class SeedViewModel { self.backupInfo = backupInfo } catch { - self.seedViewError = .generic(message: error.localizedDescription) - self.showingSeedViewErrorAlert = true + self.walletRecoveryViewError = .generic(message: error.localizedDescription) + self.showingWalletRecoveryViewErrorAlert = true } } diff --git a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift index c5d6a77c..8cb6c2bd 100644 --- a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift +++ b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift @@ -107,7 +107,7 @@ struct SettingsView: View { } .sheet(isPresented: $isSeedPresented) { - SeedView(viewModel: .init()) + WalletRecoveryView(viewModel: .init()) .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) } diff --git a/BDKSwiftExampleWallet/View/Settings/SeedView.swift b/BDKSwiftExampleWallet/View/Settings/WalletRecoveryView.swift similarity index 92% rename from BDKSwiftExampleWallet/View/Settings/SeedView.swift rename to BDKSwiftExampleWallet/View/Settings/WalletRecoveryView.swift index 93e97585..f71043e4 100644 --- a/BDKSwiftExampleWallet/View/Settings/SeedView.swift +++ b/BDKSwiftExampleWallet/View/Settings/WalletRecoveryView.swift @@ -1,5 +1,5 @@ // -// SeedView.swift +// WalletRecoveryView.swift // BDKSwiftExampleWallet // // Created by Matthew Ramsden on 1/31/24. @@ -8,8 +8,8 @@ import BitcoinUI import SwiftUI -struct SeedView: View { - @Bindable var viewModel: SeedViewModel +struct WalletRecoveryView: View { + @Bindable var viewModel: WalletRecoveryViewModel @State private var isCopied = false @State private var showCheckmark = false @@ -130,12 +130,12 @@ struct SeedView: View { viewModel.getBackupInfo(network: network) } } - .alert(isPresented: $viewModel.showingSeedViewErrorAlert) { + .alert(isPresented: $viewModel.showingWalletRecoveryViewErrorAlert) { Alert( title: Text("Showing Seed Error"), - message: Text(viewModel.seedViewError?.description ?? ""), + message: Text(viewModel.walletRecoveryViewError?.description ?? ""), dismissButton: .default(Text("OK")) { - viewModel.seedViewError = nil + viewModel.walletRecoveryViewError = nil } ) } @@ -145,6 +145,6 @@ struct SeedView: View { #if DEBUG #Preview { - SeedView(viewModel: .init(bdkClient: .mock)) + WalletRecoveryView(viewModel: .init(bdkClient: .mock)) } #endif From 3a91a130a75382452ffb66d8e9cec624296742d4 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 9 Nov 2024 17:52:59 -0600 Subject: [PATCH 4/9] disable send when no prv --- BDKSwiftExampleWallet/View Model/WalletViewModel.swift | 7 +++++++ BDKSwiftExampleWallet/View/WalletView.swift | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index db1431bb..9409413b 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -13,9 +13,14 @@ import Observation @Observable class WalletViewModel { let bdkClient: BDKClient + let keyClient: KeyClient let priceClient: PriceClient var balanceTotal: UInt64 = 0 + var canSend: Bool { + guard let backupInfo = try? keyClient.getBackupInfo() else { return false } + return backupInfo.descriptor.contains("tprv") || backupInfo.descriptor.contains("xprv") + } var inspectedScripts: UInt64 = 0 var price: Double = 0.00 var progress: Float = 0.0 @@ -35,11 +40,13 @@ class WalletViewModel { init( bdkClient: BDKClient = .live, + keyClient: KeyClient = .live, priceClient: PriceClient = .live, transactions: [CanonicalTx] = [], walletSyncState: WalletSyncState = .notStarted ) { self.bdkClient = bdkClient + self.keyClient = keyClient self.priceClient = priceClient self.transactions = transactions self.walletSyncState = walletSyncState diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index b9c62370..6ed2be54 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -191,8 +191,9 @@ struct WalletView: View { NavigationLink(value: NavigationDestination.address) { Image(systemName: "qrcode.viewfinder") .font(.title) - .foregroundStyle(.primary) + .foregroundStyle(viewModel.canSend ? .primary : .secondary) } + .disabled(!viewModel.canSend) } .padding([.horizontal, .bottom]) From 67a70a9dea37bf0da87b68e8dd48e2fb709be86e Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 9 Nov 2024 20:10:35 -0600 Subject: [PATCH 5/9] do xpubs too --- .../Service/BDK Service/BDKService.swift | 51 +++++++++++++++++++ .../View Model/OnboardingViewModel.swift | 25 ++++----- .../View/OnboardingView.swift | 10 ++-- 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index f82d93a3..a32d5b37 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -195,6 +195,52 @@ private class BDKService { self.wallet = wallet } + func createWallet(xpub: String?) throws { + let documentsDirectoryURL = URL.documentsDirectory + let walletDataDirectoryURL = documentsDirectoryURL.appendingPathComponent("wallet_data") + + if FileManager.default.fileExists(atPath: walletDataDirectoryURL.path) { + try FileManager.default.removeItem(at: walletDataDirectoryURL) + } else { + } + + let baseUrl = + try keyClient.getEsploraURL() ?? Constants.Config.EsploraServerURLNetwork.Signet.mutiny + + guard let xpubString = xpub, !xpubString.isEmpty else { + throw WalletError.walletNotFound + } + + let descriptorString = "tr(\(xpubString)/0/*)" + let changeDescriptorString = "tr(\(xpubString)/1/*)" + let descriptor = try Descriptor(descriptor: descriptorString, network: network) + let changeDescriptor = try Descriptor(descriptor: changeDescriptorString, network: network) + + let backupInfo = BackupInfo( + mnemonic: "", + descriptor: descriptor.toStringWithSecret(), + changeDescriptor: changeDescriptor.toStringWithSecret() + ) + + try keyClient.saveBackupInfo(backupInfo) + try keyClient.saveNetwork(self.network.description) + try keyClient.saveEsploraURL(baseUrl) + + try FileManager.default.ensureDirectoryExists(at: walletDataDirectoryURL) + try FileManager.default.removeOldFlatFileIfNeeded(at: documentsDirectoryURL) + let persistenceBackendPath = walletDataDirectoryURL.appendingPathComponent("wallet.sqlite") + .path + let connection = try Connection(path: persistenceBackendPath) + self.connection = connection + let wallet = try Wallet( + descriptor: descriptor, + changeDescriptor: changeDescriptor, + network: network, + connection: connection + ) + self.wallet = wallet + } + private func loadWallet(descriptor: Descriptor, changeDescriptor: Descriptor) throws { let documentsDirectoryURL = URL.documentsDirectory let walletDataDirectoryURL = documentsDirectoryURL.appendingPathComponent("wallet_data") @@ -362,6 +408,7 @@ struct BDKClient { let deleteWallet: () throws -> Void let createWalletFromSeed: (String?) throws -> Void let createWalletFromDescriptor: (String?) throws -> Void + let createWalletFromXPub: (String?) throws -> Void let getBalance: () throws -> Balance let transactions: () throws -> [CanonicalTx] let listUnspent: () throws -> [LocalOutput] @@ -390,6 +437,9 @@ extension BDKClient { createWalletFromDescriptor: { descriptor in try BDKService.shared.createWallet(descriptor: descriptor) }, + createWalletFromXPub: { xpub in + try BDKService.shared.createWallet(xpub: xpub) + }, getBalance: { try BDKService.shared.getBalance() }, transactions: { try BDKService.shared.transactions() }, listUnspent: { try BDKService.shared.listUnspent() }, @@ -440,6 +490,7 @@ extension BDKClient { deleteWallet: {}, createWalletFromSeed: { _ in }, createWalletFromDescriptor: { _ in }, + createWalletFromXPub: { _ in }, getBalance: { .mock }, transactions: { return [ diff --git a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift index 5c527c27..64792d44 100644 --- a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift @@ -15,13 +15,15 @@ import SwiftUI class OnboardingViewModel: ObservableObject { let bdkClient: BDKClient + @AppStorage("isOnboarding") var isOnboarding: Bool? + @Published var createWithPersistError: CreateWithPersistError? var isDescriptor: Bool { words.hasPrefix("tr(") || words.hasPrefix("wpkh(") || words.hasPrefix("wsh(") || words.hasPrefix("sh(") } - - @AppStorage("isOnboarding") var isOnboarding: Bool? - @Published var createWithPersistError: CreateWithPersistError? + var isXPub: Bool { + words.hasPrefix("xpub") || words.hasPrefix("tpub") + } @Published var networkColor = Color.gray @Published var onboardingViewError: AppError? @Published var selectedNetwork: Network = .signet { @@ -36,12 +38,14 @@ class OnboardingViewModel: ObservableObject { bdkClient.updateEsploraURL(selectedURL) } } - @Published var words: String = "" { - didSet { - updateWordArray() + @Published var words: String = "" + var wordArray: [String] { + if words.hasPrefix("xpub") || words.hasPrefix("tpub") { + return [] } + let trimmedWords = words.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmedWords.components(separatedBy: " ") } - @Published var wordArray: [String] = [] var availableURLs: [String] { switch selectedNetwork { case .bitcoin: @@ -79,6 +83,8 @@ class OnboardingViewModel: ObservableObject { do { if isDescriptor { try bdkClient.createWalletFromDescriptor(words) + } else if isXPub { + try bdkClient.createWalletFromXPub(words) } else { try bdkClient.createWalletFromSeed(words) } @@ -95,9 +101,4 @@ class OnboardingViewModel: ObservableObject { } } } - - private func updateWordArray() { - let trimmedWords = words.trimmingCharacters(in: .whitespacesAndNewlines) - wordArray = trimmedWords.split(separator: " ").map { String($0) } - } } diff --git a/BDKSwiftExampleWallet/View/OnboardingView.swift b/BDKSwiftExampleWallet/View/OnboardingView.swift index d857e24d..87709f09 100644 --- a/BDKSwiftExampleWallet/View/OnboardingView.swift +++ b/BDKSwiftExampleWallet/View/OnboardingView.swift @@ -27,7 +27,7 @@ struct OnboardingView: View { HStack { Spacer() Button { - if viewModel.wordArray.isEmpty { + if viewModel.words.isEmpty { if let clipboardContent = UIPasteboard.general.string { viewModel.words = clipboardContent } @@ -36,13 +36,13 @@ struct OnboardingView: View { } } label: { Image( - systemName: viewModel.wordArray.isEmpty + systemName: viewModel.words.isEmpty ? "arrow.down.square" : "clear" ) .contentTransition(.symbolEffect(.replace)) } .tint( - viewModel.wordArray.isEmpty ? .secondary : .primary + viewModel.words.isEmpty ? .secondary : .primary ) .font(.title) .padding() @@ -108,8 +108,8 @@ struct OnboardingView: View { .pickerStyle(.automatic) .tint(.primary) - if viewModel.wordArray != [] { - if viewModel.isDescriptor { + if !viewModel.words.isEmpty { + if viewModel.isDescriptor || viewModel.isXPub { Text(viewModel.words) .font(.system(.caption, design: .monospaced)) .lineLimit(1) From e1e10891a8563579c75ef4346da02469886dcc6b Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 10 Nov 2024 13:49:14 -0600 Subject: [PATCH 6/9] recovery view show helper text --- .../View/Settings/WalletRecoveryView.swift | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/BDKSwiftExampleWallet/View/Settings/WalletRecoveryView.swift b/BDKSwiftExampleWallet/View/Settings/WalletRecoveryView.swift index f71043e4..08e648ad 100644 --- a/BDKSwiftExampleWallet/View/Settings/WalletRecoveryView.swift +++ b/BDKSwiftExampleWallet/View/Settings/WalletRecoveryView.swift @@ -39,16 +39,14 @@ struct WalletRecoveryView: View { ) } - if !backupInfo.mnemonic.isEmpty { - VStack { - Text("Seed is not synced across devices.") - Text("Please make sure to write it down and store it securely.") - } - .font(.caption) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .padding() + VStack { + Text("Backup is not synced across devices.") + Text("Please make sure to write it down and store it securely.") } + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding() if !backupInfo.mnemonic.isEmpty { HStack { From dd4ec9381d4f0ff8a94924c285ea435d85688d2f Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 10 Nov 2024 14:32:24 -0600 Subject: [PATCH 7/9] scan desc+xpub --- .../Resources/Localizable.xcstrings | 6 ++-- .../View Model/OnboardingViewModel.swift | 4 +-- .../View/OnboardingView.swift | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings index c85c2604..7765d1f2 100644 --- a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings +++ b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings @@ -243,6 +243,9 @@ } } } + }, + "Backup is not synced across devices." : { + }, "BDK Wallet" : { "extractionState" : "stale", @@ -590,9 +593,6 @@ }, "Seed" : { - }, - "Seed is not synced across devices." : { - }, "Select Bitcoin Network" : { diff --git a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift index 64792d44..012b2e18 100644 --- a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift @@ -22,7 +22,7 @@ class OnboardingViewModel: ObservableObject { || words.hasPrefix("sh(") } var isXPub: Bool { - words.hasPrefix("xpub") || words.hasPrefix("tpub") + words.hasPrefix("xpub") || words.hasPrefix("tpub") || words.hasPrefix("vpub") } @Published var networkColor = Color.gray @Published var onboardingViewError: AppError? @@ -40,7 +40,7 @@ class OnboardingViewModel: ObservableObject { } @Published var words: String = "" var wordArray: [String] { - if words.hasPrefix("xpub") || words.hasPrefix("tpub") { + if words.hasPrefix("xpub") || words.hasPrefix("tpub") || words.hasPrefix("vpub") { return [] } let trimmedWords = words.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/BDKSwiftExampleWallet/View/OnboardingView.swift b/BDKSwiftExampleWallet/View/OnboardingView.swift index 87709f09..2e6adaee 100644 --- a/BDKSwiftExampleWallet/View/OnboardingView.swift +++ b/BDKSwiftExampleWallet/View/OnboardingView.swift @@ -14,6 +14,7 @@ struct OnboardingView: View { @ObservedObject var viewModel: OnboardingViewModel @State private var showingOnboardingViewErrorAlert = false @State private var showingImportView = false + @State private var showingScanner = false let pasteboard = UIPasteboard.general var body: some View { @@ -25,7 +26,24 @@ struct OnboardingView: View { VStack { HStack { + Spacer() + + Button { + showingScanner = true + } label: { + Image( + systemName: viewModel.words.isEmpty + ? "qrcode.viewfinder" : "clear" + ) + .contentTransition(.symbolEffect(.replace)) + } + .tint( + viewModel.words.isEmpty ? .secondary : .primary + ) + .font(.title) + .padding() + Button { if viewModel.words.isEmpty { if let clipboardContent = UIPasteboard.general.string { @@ -153,6 +171,24 @@ struct OnboardingView: View { } ) } + .sheet(isPresented: $showingScanner) { + CustomScannerView( + codeTypes: [.qr], + completion: { result in + switch result { + case .success(let result): + viewModel.words = result.string + showingScanner = false + case .failure(let error): + viewModel.onboardingViewError = .generic( + message: error.localizedDescription + ) + showingScanner = false + } + }, + pasteAction: {} + ) + } } } From 94698cb988e75121cfb1082180c3094f7f5a1efb Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 10 Nov 2024 15:49:44 -0600 Subject: [PATCH 8/9] onboarding buttons only have one clear button --- .../View/OnboardingView.swift | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/BDKSwiftExampleWallet/View/OnboardingView.swift b/BDKSwiftExampleWallet/View/OnboardingView.swift index 2e6adaee..08dd1a8d 100644 --- a/BDKSwiftExampleWallet/View/OnboardingView.swift +++ b/BDKSwiftExampleWallet/View/OnboardingView.swift @@ -29,41 +29,39 @@ struct OnboardingView: View { Spacer() - Button { - showingScanner = true - } label: { - Image( - systemName: viewModel.words.isEmpty - ? "qrcode.viewfinder" : "clear" - ) - .contentTransition(.symbolEffect(.replace)) - } - .tint( - viewModel.words.isEmpty ? .secondary : .primary - ) - .font(.title) - .padding() + if viewModel.words.isEmpty { + Button { + showingScanner = true + } label: { + Image(systemName: "qrcode.viewfinder") + .contentTransition(.symbolEffect(.replace)) + } + .tint(.secondary) + .font(.title) + .padding() - Button { - if viewModel.words.isEmpty { + Button { if let clipboardContent = UIPasteboard.general.string { viewModel.words = clipboardContent } - } else { + } label: { + Image(systemName: "arrow.down.square") + .contentTransition(.symbolEffect(.replace)) + } + .tint(.secondary) + .font(.title) + .padding() + } else { + Button { viewModel.words = "" + } label: { + Image(systemName: "clear") + .contentTransition(.symbolEffect(.replace)) } - } label: { - Image( - systemName: viewModel.words.isEmpty - ? "arrow.down.square" : "clear" - ) - .contentTransition(.symbolEffect(.replace)) + .tint(.primary) + .font(.title) + .padding() } - .tint( - viewModel.words.isEmpty ? .secondary : .primary - ) - .font(.title) - .padding() } Spacer() From b9f82cc6067b7b25f34c5647d8acd5b4de11afce Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 10 Nov 2024 15:57:45 -0600 Subject: [PATCH 9/9] onboarding buttons only have one clear button transition --- BDKSwiftExampleWallet/View/OnboardingView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BDKSwiftExampleWallet/View/OnboardingView.swift b/BDKSwiftExampleWallet/View/OnboardingView.swift index 08dd1a8d..8c9bf618 100644 --- a/BDKSwiftExampleWallet/View/OnboardingView.swift +++ b/BDKSwiftExampleWallet/View/OnboardingView.swift @@ -34,7 +34,7 @@ struct OnboardingView: View { showingScanner = true } label: { Image(systemName: "qrcode.viewfinder") - .contentTransition(.symbolEffect(.replace)) + .transition(.symbolEffect(.disappear)) } .tint(.secondary) .font(.title)