Skip to content

Commit 121b6bb

Browse files
authored
feat: import descriptor+xpub
1 parent 9b2542f commit 121b6bb

File tree

10 files changed

+284
-104
lines changed

10 files changed

+284
-104
lines changed

BDKSwiftExampleWallet.xcodeproj/project.pbxproj

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
AE2B8C1D2A9678C900815B2F /* FeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE2B8C1C2A9678C900815B2F /* FeeService.swift */; };
3535
AE2B8C1F2A96797300815B2F /* RecommendedFees.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE2B8C1E2A96797300815B2F /* RecommendedFees.swift */; };
3636
AE2F255D2BED0BFB002A9AC6 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE2F255C2BED0BFB002A9AC6 /* AppError.swift */; };
37-
AE34DDAC2B6B31ED00F04AD4 /* SeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAB2B6B31ED00F04AD4 /* SeedView.swift */; };
38-
AE34DDAE2B6B320F00F04AD4 /* SeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAD2B6B320F00F04AD4 /* SeedViewModel.swift */; };
37+
AE34DDAC2B6B31ED00F04AD4 /* WalletRecoveryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAB2B6B31ED00F04AD4 /* WalletRecoveryView.swift */; };
38+
AE34DDAE2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAD2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift */; };
3939
AE3646262BEDB01200B04E25 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3646252BEDB01200B04E25 /* FileManager+Extensions.swift */; };
4040
AE3902A42A3B4CD900BEC318 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3902A32A3B4CD900BEC318 /* HomeView.swift */; };
4141
AE49847C2A1BBBD6009951E2 /* BDKSwiftExampleWalletApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE49847B2A1BBBD6009951E2 /* BDKSwiftExampleWalletApp.swift */; };
@@ -126,8 +126,8 @@
126126
AE2B8C1C2A9678C900815B2F /* FeeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeService.swift; sourceTree = "<group>"; };
127127
AE2B8C1E2A96797300815B2F /* RecommendedFees.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedFees.swift; sourceTree = "<group>"; };
128128
AE2F255C2BED0BFB002A9AC6 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
129-
AE34DDAB2B6B31ED00F04AD4 /* SeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedView.swift; sourceTree = "<group>"; };
130-
AE34DDAD2B6B320F00F04AD4 /* SeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedViewModel.swift; sourceTree = "<group>"; };
129+
AE34DDAB2B6B31ED00F04AD4 /* WalletRecoveryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletRecoveryView.swift; sourceTree = "<group>"; };
130+
AE34DDAD2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletRecoveryViewModel.swift; sourceTree = "<group>"; };
131131
AE3646252BEDB01200B04E25 /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = "<group>"; };
132132
AE3902A32A3B4CD900BEC318 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
133133
AE4984782A1BBBD6009951E2 /* BDKSwiftExampleWallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BDKSwiftExampleWallet.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -303,7 +303,7 @@
303303
isa = PBXGroup;
304304
children = (
305305
AE2ADD732B61E8F500C2A823 /* SettingsView.swift */,
306-
AE34DDAB2B6B31ED00F04AD4 /* SeedView.swift */,
306+
AE34DDAB2B6B31ED00F04AD4 /* WalletRecoveryView.swift */,
307307
);
308308
path = Settings;
309309
sourceTree = "<group>";
@@ -359,7 +359,7 @@
359359
isa = PBXGroup;
360360
children = (
361361
AE2ADD772B61EFFE00C2A823 /* SettingsViewModel.swift */,
362-
AE34DDAD2B6B320F00F04AD4 /* SeedViewModel.swift */,
362+
AE34DDAD2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift */,
363363
);
364364
path = Settings;
365365
sourceTree = "<group>";
@@ -665,7 +665,7 @@
665665
AEB130C92A44E4850087785B /* TransactionDetailView.swift in Sources */,
666666
AE287E772C0F6D200036A748 /* Array+Extensions.swift in Sources */,
667667
AE6715FD2A9AC056005C193F /* PriceServiceError.swift in Sources */,
668-
AE34DDAC2B6B31ED00F04AD4 /* SeedView.swift in Sources */,
668+
AE34DDAC2B6B31ED00F04AD4 /* WalletRecoveryView.swift in Sources */,
669669
AE2ADD742B61E8F500C2A823 /* SettingsView.swift in Sources */,
670670
AE2381AF2C605B1D00F6B00C /* ActivityListViewModel.swift in Sources */,
671671
AE6F34D82AA6C1800087E700 /* Network+Extensions.swift in Sources */,
@@ -689,7 +689,7 @@
689689
AE2381B32C60877600F6B00C /* LocalOutputListView.swift in Sources */,
690690
AE783A052AB4F51F005F0CBA /* String+Extensions.swift in Sources */,
691691
AE29ED112BBE318A00EB9C4F /* TransactionItemView.swift in Sources */,
692-
AE34DDAE2B6B320F00F04AD4 /* SeedViewModel.swift in Sources */,
692+
AE34DDAE2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift in Sources */,
693693
AE0C30F92A804B65008F1EAE /* OnboardingViewModel.swift in Sources */,
694694
AE3902A42A3B4CD900BEC318 /* HomeView.swift in Sources */,
695695
AE0C30FD2A804BC1008F1EAE /* ReceiveViewModel.swift in Sources */,

BDKSwiftExampleWallet/Resources/Localizable.xcstrings

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@
243243
}
244244
}
245245
}
246+
},
247+
"Backup is not synced across devices." : {
248+
246249
},
247250
"BDK Wallet" : {
248251
"extractionState" : "stale",
@@ -320,6 +323,7 @@
320323
}
321324
},
322325
"Delete Seed" : {
326+
"extractionState" : "stale",
323327
"localizations" : {
324328
"fr" : {
325329
"stringUnit" : {
@@ -328,6 +332,9 @@
328332
}
329333
}
330334
}
335+
},
336+
"Delete Wallet" : {
337+
331338
},
332339
"Descriptors" : {
333340

@@ -586,9 +593,6 @@
586593
},
587594
"Seed" : {
588595

589-
},
590-
"Seed is not synced across devices." : {
591-
592596
},
593597
"Select Bitcoin Network" : {
594598

@@ -640,6 +644,7 @@
640644
}
641645
},
642646
"Show Seed" : {
647+
"extractionState" : "stale",
643648
"localizations" : {
644649
"fr" : {
645650
"stringUnit" : {
@@ -648,6 +653,9 @@
648653
}
649654
}
650655
}
656+
},
657+
"Show Wallet" : {
658+
651659
},
652660
"Showing Seed Error" : {
653661
"localizations" : {

BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,99 @@ private class BDKService {
148148
self.wallet = wallet
149149
}
150150

151+
func createWallet(descriptor: String?) throws {
152+
let documentsDirectoryURL = URL.documentsDirectory
153+
let walletDataDirectoryURL = documentsDirectoryURL.appendingPathComponent("wallet_data")
154+
155+
if FileManager.default.fileExists(atPath: walletDataDirectoryURL.path) {
156+
try FileManager.default.removeItem(at: walletDataDirectoryURL)
157+
} else {
158+
}
159+
160+
let baseUrl =
161+
try keyClient.getEsploraURL() ?? Constants.Config.EsploraServerURLNetwork.Signet.mutiny
162+
163+
guard let descriptorString = descriptor, !descriptorString.isEmpty else {
164+
throw WalletError.walletNotFound
165+
}
166+
167+
let cleanDescriptor =
168+
descriptorString.split(separator: "#").first.map(String.init) ?? descriptorString
169+
let descriptor = try Descriptor(descriptor: cleanDescriptor, network: network)
170+
let changeDescriptorString = cleanDescriptor.replacingOccurrences(of: "/0/*", with: "/1/*")
171+
let changeDescriptor = try Descriptor(descriptor: changeDescriptorString, network: network)
172+
173+
let backupInfo = BackupInfo(
174+
mnemonic: "",
175+
descriptor: descriptor.toStringWithSecret(),
176+
changeDescriptor: changeDescriptor.toStringWithSecret()
177+
)
178+
179+
try keyClient.saveBackupInfo(backupInfo)
180+
try keyClient.saveNetwork(self.network.description)
181+
try keyClient.saveEsploraURL(baseUrl)
182+
183+
try FileManager.default.ensureDirectoryExists(at: walletDataDirectoryURL)
184+
try FileManager.default.removeOldFlatFileIfNeeded(at: documentsDirectoryURL)
185+
let persistenceBackendPath = walletDataDirectoryURL.appendingPathComponent("wallet.sqlite")
186+
.path
187+
let connection = try Connection(path: persistenceBackendPath)
188+
self.connection = connection
189+
let wallet = try Wallet(
190+
descriptor: descriptor,
191+
changeDescriptor: changeDescriptor,
192+
network: network,
193+
connection: connection
194+
)
195+
self.wallet = wallet
196+
}
197+
198+
func createWallet(xpub: String?) throws {
199+
let documentsDirectoryURL = URL.documentsDirectory
200+
let walletDataDirectoryURL = documentsDirectoryURL.appendingPathComponent("wallet_data")
201+
202+
if FileManager.default.fileExists(atPath: walletDataDirectoryURL.path) {
203+
try FileManager.default.removeItem(at: walletDataDirectoryURL)
204+
} else {
205+
}
206+
207+
let baseUrl =
208+
try keyClient.getEsploraURL() ?? Constants.Config.EsploraServerURLNetwork.Signet.mutiny
209+
210+
guard let xpubString = xpub, !xpubString.isEmpty else {
211+
throw WalletError.walletNotFound
212+
}
213+
214+
let descriptorString = "tr(\(xpubString)/0/*)"
215+
let changeDescriptorString = "tr(\(xpubString)/1/*)"
216+
let descriptor = try Descriptor(descriptor: descriptorString, network: network)
217+
let changeDescriptor = try Descriptor(descriptor: changeDescriptorString, network: network)
218+
219+
let backupInfo = BackupInfo(
220+
mnemonic: "",
221+
descriptor: descriptor.toStringWithSecret(),
222+
changeDescriptor: changeDescriptor.toStringWithSecret()
223+
)
224+
225+
try keyClient.saveBackupInfo(backupInfo)
226+
try keyClient.saveNetwork(self.network.description)
227+
try keyClient.saveEsploraURL(baseUrl)
228+
229+
try FileManager.default.ensureDirectoryExists(at: walletDataDirectoryURL)
230+
try FileManager.default.removeOldFlatFileIfNeeded(at: documentsDirectoryURL)
231+
let persistenceBackendPath = walletDataDirectoryURL.appendingPathComponent("wallet.sqlite")
232+
.path
233+
let connection = try Connection(path: persistenceBackendPath)
234+
self.connection = connection
235+
let wallet = try Wallet(
236+
descriptor: descriptor,
237+
changeDescriptor: changeDescriptor,
238+
network: network,
239+
connection: connection
240+
)
241+
self.wallet = wallet
242+
}
243+
151244
private func loadWallet(descriptor: Descriptor, changeDescriptor: Descriptor) throws {
152245
let documentsDirectoryURL = URL.documentsDirectory
153246
let walletDataDirectoryURL = documentsDirectoryURL.appendingPathComponent("wallet_data")
@@ -313,7 +406,9 @@ extension BDKService {
313406
struct BDKClient {
314407
let loadWallet: () throws -> Void
315408
let deleteWallet: () throws -> Void
316-
let createWallet: (String?) throws -> Void
409+
let createWalletFromSeed: (String?) throws -> Void
410+
let createWalletFromDescriptor: (String?) throws -> Void
411+
let createWalletFromXPub: (String?) throws -> Void
317412
let getBalance: () throws -> Balance
318413
let transactions: () throws -> [CanonicalTx]
319414
let listUnspent: () throws -> [LocalOutput]
@@ -338,7 +433,13 @@ extension BDKClient {
338433
static let live = Self(
339434
loadWallet: { try BDKService.shared.loadWalletFromBackup() },
340435
deleteWallet: { try BDKService.shared.deleteWallet() },
341-
createWallet: { words in try BDKService.shared.createWallet(words: words) },
436+
createWalletFromSeed: { words in try BDKService.shared.createWallet(words: words) },
437+
createWalletFromDescriptor: { descriptor in
438+
try BDKService.shared.createWallet(descriptor: descriptor)
439+
},
440+
createWalletFromXPub: { xpub in
441+
try BDKService.shared.createWallet(xpub: xpub)
442+
},
342443
getBalance: { try BDKService.shared.getBalance() },
343444
transactions: { try BDKService.shared.transactions() },
344445
listUnspent: { try BDKService.shared.listUnspent() },
@@ -387,7 +488,9 @@ extension BDKClient {
387488
static let mock = Self(
388489
loadWallet: {},
389490
deleteWallet: {},
390-
createWallet: { _ in },
491+
createWalletFromSeed: { _ in },
492+
createWalletFromDescriptor: { _ in },
493+
createWalletFromXPub: { _ in },
391494
getBalance: { .mock },
392495
transactions: {
393496
return [

BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ class OnboardingViewModel: ObservableObject {
1717

1818
@AppStorage("isOnboarding") var isOnboarding: Bool?
1919
@Published var createWithPersistError: CreateWithPersistError?
20+
var isDescriptor: Bool {
21+
words.hasPrefix("tr(") || words.hasPrefix("wpkh(") || words.hasPrefix("wsh(")
22+
|| words.hasPrefix("sh(")
23+
}
24+
var isXPub: Bool {
25+
words.hasPrefix("xpub") || words.hasPrefix("tpub") || words.hasPrefix("vpub")
26+
}
2027
@Published var networkColor = Color.gray
2128
@Published var onboardingViewError: AppError?
2229
@Published var selectedNetwork: Network = .signet {
@@ -31,12 +38,14 @@ class OnboardingViewModel: ObservableObject {
3138
bdkClient.updateEsploraURL(selectedURL)
3239
}
3340
}
34-
@Published var words: String = "" {
35-
didSet {
36-
updateWordArray()
41+
@Published var words: String = ""
42+
var wordArray: [String] {
43+
if words.hasPrefix("xpub") || words.hasPrefix("tpub") || words.hasPrefix("vpub") {
44+
return []
3745
}
46+
let trimmedWords = words.trimmingCharacters(in: .whitespacesAndNewlines)
47+
return trimmedWords.components(separatedBy: " ")
3848
}
39-
@Published var wordArray: [String] = []
4049
var availableURLs: [String] {
4150
switch selectedNetwork {
4251
case .bitcoin:
@@ -72,7 +81,13 @@ class OnboardingViewModel: ObservableObject {
7281

7382
func createWallet() {
7483
do {
75-
try bdkClient.createWallet(words)
84+
if isDescriptor {
85+
try bdkClient.createWalletFromDescriptor(words)
86+
} else if isXPub {
87+
try bdkClient.createWalletFromXPub(words)
88+
} else {
89+
try bdkClient.createWalletFromSeed(words)
90+
}
7691
DispatchQueue.main.async {
7792
self.isOnboarding = false
7893
}
@@ -86,9 +101,4 @@ class OnboardingViewModel: ObservableObject {
86101
}
87102
}
88103
}
89-
90-
private func updateWordArray() {
91-
let trimmedWords = words.trimmingCharacters(in: .whitespacesAndNewlines)
92-
wordArray = trimmedWords.split(separator: " ").map { String($0) }
93-
}
94104
}

BDKSwiftExampleWallet/View Model/Settings/SeedViewModel.swift renamed to BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// SeedViewModel.swift
2+
// WalletRecoveryViewModel.swift
33
// BDKSwiftExampleWallet
44
//
55
// Created by Matthew Ramsden on 1/31/24.
@@ -11,25 +11,25 @@ import SwiftUI
1111

1212
@Observable
1313
@MainActor
14-
class SeedViewModel {
14+
class WalletRecoveryViewModel {
1515
let bdkClient: BDKClient
1616

1717
var backupInfo: BackupInfo?
1818
var publicDescriptor: Descriptor?
1919
var publicChangeDescriptor: Descriptor?
20-
var seedViewError: AppError?
21-
var showingSeedViewErrorAlert: Bool
20+
var walletRecoveryViewError: AppError?
21+
var showingWalletRecoveryViewErrorAlert: Bool
2222

2323
init(
2424
bdkClient: BDKClient = .live,
2525
backupInfo: BackupInfo? = nil,
26-
seedViewError: AppError? = nil,
27-
showingSeedViewErrorAlert: Bool = false
26+
walletRecoveryViewError: AppError? = nil,
27+
showingWalletRecoveryViewErrorAlert: Bool = false
2828
) {
2929
self.bdkClient = bdkClient
3030
self.backupInfo = backupInfo
31-
self.seedViewError = seedViewError
32-
self.showingSeedViewErrorAlert = showingSeedViewErrorAlert
31+
self.walletRecoveryViewError = walletRecoveryViewError
32+
self.showingWalletRecoveryViewErrorAlert = showingWalletRecoveryViewErrorAlert
3333
}
3434

3535
func getNetwork() -> Network {
@@ -55,8 +55,8 @@ class SeedViewModel {
5555

5656
self.backupInfo = backupInfo
5757
} catch {
58-
self.seedViewError = .generic(message: error.localizedDescription)
59-
self.showingSeedViewErrorAlert = true
58+
self.walletRecoveryViewError = .generic(message: error.localizedDescription)
59+
self.showingWalletRecoveryViewErrorAlert = true
6060
}
6161
}
6262

BDKSwiftExampleWallet/View Model/WalletViewModel.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ import Observation
1313
@Observable
1414
class WalletViewModel {
1515
let bdkClient: BDKClient
16+
let keyClient: KeyClient
1617
let priceClient: PriceClient
1718

1819
var balanceTotal: UInt64 = 0
20+
var canSend: Bool {
21+
guard let backupInfo = try? keyClient.getBackupInfo() else { return false }
22+
return backupInfo.descriptor.contains("tprv") || backupInfo.descriptor.contains("xprv")
23+
}
1924
var inspectedScripts: UInt64 = 0
2025
var price: Double = 0.00
2126
var progress: Float = 0.0
@@ -35,11 +40,13 @@ class WalletViewModel {
3540

3641
init(
3742
bdkClient: BDKClient = .live,
43+
keyClient: KeyClient = .live,
3844
priceClient: PriceClient = .live,
3945
transactions: [CanonicalTx] = [],
4046
walletSyncState: WalletSyncState = .notStarted
4147
) {
4248
self.bdkClient = bdkClient
49+
self.keyClient = keyClient
4350
self.priceClient = priceClient
4451
self.transactions = transactions
4552
self.walletSyncState = walletSyncState

0 commit comments

Comments
 (0)