From 2e3aaae74fec52fb92f2ec777d9ef4548bb673df Mon Sep 17 00:00:00 2001 From: Matthew <6657488+reez@users.noreply.github.com> Date: Mon, 29 Jan 2024 10:36:56 -0600 Subject: [PATCH] feat: add network --- .../BDK+Extensions/Network+Extensions.swift | 21 ++++ .../Resources/Localizable.xcstrings | 21 ++++ .../Service/BDK Service/BDKService.swift | 41 +++++-- .../Service/Key Service/KeyService.swift | 63 ++++++++++- .../Utilities/Constants.swift | 52 ++++++++- .../View Model/OnboardingViewModel.swift | 69 ++++++++++++ .../View Model/SettingsViewModel.swift | 104 +++++++++++++++++- .../View/OnboardingView.swift | 45 ++++++++ BDKSwiftExampleWallet/View/SettingsView.swift | 22 ++++ 9 files changed, 423 insertions(+), 15 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/Network+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/Network+Extensions.swift index e386c54..d41880b 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/Network+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/Network+Extensions.swift @@ -8,6 +8,27 @@ import BitcoinDevKit import Foundation +extension Network { + var description: String { + switch self { + case .bitcoin: return "bitcoin" + case .testnet: return "testnet" + case .signet: return "signet" + case .regtest: return "regtest" + } + } + + init?(stringValue: String) { + switch stringValue { + case "bitcoin": self = .bitcoin + case "testnet": self = .testnet + case "signet": self = .signet + case "regtest": self = .regtest + default: return nil + } + } +} + #if DEBUG let mockKeyClientNetwork = Network.regtest #endif diff --git a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings index f32a5b9..afc8fe9 100644 --- a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings +++ b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings @@ -136,6 +136,9 @@ }, "Are you sure you want to delete the seed?" : { + }, + "Bitcoin" : { + }, "Bitcoin Wallet" : { "localizations" : { @@ -156,6 +159,9 @@ } } } + }, + "Choose your Network." : { + }, "confirmed" : { "localizations" : { @@ -216,6 +222,9 @@ } } } + }, + "Esplora Server" : { + }, "Fee" : { "localizations" : { @@ -236,6 +245,9 @@ } } } + }, + "Network" : { + }, "Next" : { "localizations" : { @@ -279,6 +291,9 @@ } } } + }, + "Regtest" : { + }, "sat/vb" : { "localizations" : { @@ -320,6 +335,9 @@ } } } + }, + "Signet" : { + }, "Syncing" : { "extractionState" : "manual", @@ -331,6 +349,9 @@ } } } + }, + "Testnet" : { + }, "To" : { "localizations" : { diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 1a50f1e..d405ebf 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -9,22 +9,26 @@ import BitcoinDevKit import Foundation private class BDKService { + static var shared: BDKService = BDKService() private var balance: Balance? private var blockchainConfig: BlockchainConfig? - var network: Network = .signet + var network: Network private var wallet: Wallet? private let keyService: KeyClient - class var shared: BDKService { - struct Singleton { - static let instance = BDKService() - } - return Singleton.instance - } + init( + keyService: KeyClient = .live + ) { + let storedNetworkString = try! keyService.getNetwork() ?? Network.testnet.description + let storedEsploraURL = + try! keyService.getEsploraURL() + ?? Constants.Config.EsploraServerURLNetwork.Testnet.mempoolspace + + self.network = Network(stringValue: storedNetworkString) ?? .testnet + self.keyService = keyService - init(keyService: KeyClient = .live) { let esploraConfig = EsploraConfig( - baseUrl: Constants.Config.EsploraServerURLNetwork.Signet.mutiny, + baseUrl: storedEsploraURL, proxy: nil, concurrency: nil, stopGap: UInt64(20), @@ -32,7 +36,6 @@ private class BDKService { ) let blockchainConfig = BlockchainConfig.esplora(config: esploraConfig) self.blockchainConfig = blockchainConfig - self.keyService = keyService } func getAddress() throws -> String { @@ -56,6 +59,20 @@ private class BDKService { } func createWallet(words: String?) throws { + + let baseUrl = + try! keyService.getEsploraURL() + ?? Constants.Config.EsploraServerURLNetwork.Testnet.mempoolspace + let esploraConfig = EsploraConfig( + baseUrl: baseUrl, + proxy: nil, + concurrency: nil, + stopGap: UInt64(20), + timeout: nil + ) + let blockchainConfig = BlockchainConfig.esplora(config: esploraConfig) + self.blockchainConfig = blockchainConfig + var words12: String if let words = words, !words.isEmpty { words12 = words @@ -85,6 +102,8 @@ private class BDKService { changeDescriptor: changeDescriptor.asStringPrivate() ) try keyService.saveBackupInfo(backupInfo) + try keyService.saveNetwork(self.network.description) + try keyService.saveEsploraURL(baseUrl) let wallet = try Wallet( descriptor: descriptor, changeDescriptor: changeDescriptor, @@ -119,6 +138,8 @@ private class BDKService { UserDefaults.standard.removePersistentDomain(forName: bundleID) } try self.keyService.deleteBackupInfo() + try self.keyService.deleteEsplora() + try self.keyService.deleteNetwork() } func send(address: String, amount: UInt64, feeRate: Float?) throws { diff --git a/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift b/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift index 517e2d3..7c29f88 100644 --- a/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift +++ b/BDKSwiftExampleWallet/Service/Key Service/KeyService.swift @@ -40,19 +40,64 @@ private struct KeyService { } } +extension KeyService { + func saveNetwork(network: String) throws { + keychain[string: "SelectedNetwork"] = network + } + + func getNetwork() throws -> String? { + return keychain[string: "SelectedNetwork"] + } + + func deleteNetwork() throws { + try keychain.remove("SelectedNetwork") + } + + func saveEsploraURL(url: String) throws { + keychain[string: "SelectedEsploraURL"] = url + } + + func getEsploraURL() throws -> String? { + return keychain[string: "SelectedEsploraURL"] + } + + func deleteEsploraURL() throws { + try keychain.remove("SelectedEsploraURL") + } +} + struct KeyClient { let saveBackupInfo: (BackupInfo) throws -> Void let getBackupInfo: () throws -> BackupInfo let deleteBackupInfo: () throws -> Void + let saveNetwork: (String) throws -> Void + let getNetwork: () throws -> String? + let saveEsploraURL: (String) throws -> Void + let getEsploraURL: () throws -> String? + let deleteNetwork: () throws -> Void + let deleteEsplora: () throws -> Void + private init( saveBackupInfo: @escaping (BackupInfo) throws -> Void, getBackupInfo: @escaping () throws -> BackupInfo, - deleteBackupInfo: @escaping () throws -> Void + deleteBackupInfo: @escaping () throws -> Void, + saveNetwork: @escaping (String) throws -> Void, + getNetwork: @escaping () throws -> String?, + saveEsploraURL: @escaping (String) throws -> Void, + getEsploraURL: @escaping () throws -> String?, + deleteNetwork: @escaping () throws -> Void, + deleteEsplora: @escaping () throws -> Void ) { self.saveBackupInfo = saveBackupInfo self.getBackupInfo = getBackupInfo self.deleteBackupInfo = deleteBackupInfo + self.saveNetwork = saveNetwork + self.getNetwork = getNetwork + self.saveEsploraURL = saveEsploraURL + self.getEsploraURL = getEsploraURL + self.deleteNetwork = deleteNetwork + self.deleteEsplora = deleteEsplora } } @@ -60,7 +105,13 @@ extension KeyClient { static let live = Self( saveBackupInfo: { backupInfo in try KeyService().saveBackupInfo(backupInfo: backupInfo) }, getBackupInfo: { try KeyService().getBackupInfo() }, - deleteBackupInfo: { try KeyService().deleteBackupInfo() } + deleteBackupInfo: { try KeyService().deleteBackupInfo() }, + saveNetwork: { network in try KeyService().saveNetwork(network: network) }, + getNetwork: { try KeyService().getNetwork() }, + saveEsploraURL: { url in try KeyService().saveEsploraURL(url: url) }, + getEsploraURL: { try KeyService().getEsploraURL() }, + deleteNetwork: { try KeyService().deleteNetwork() }, + deleteEsplora: { try KeyService().deleteEsploraURL() } ) } @@ -94,7 +145,13 @@ extension KeyClient { ) return backupInfo }, - deleteBackupInfo: { try KeyService().deleteBackupInfo() } + deleteBackupInfo: { try KeyService().deleteBackupInfo() }, + saveNetwork: { _ in }, + getNetwork: { nil }, + saveEsploraURL: { _ in }, + getEsploraURL: { nil }, + deleteNetwork: {}, + deleteEsplora: {} ) } #endif diff --git a/BDKSwiftExampleWallet/Utilities/Constants.swift b/BDKSwiftExampleWallet/Utilities/Constants.swift index e873442..e2572a6 100644 --- a/BDKSwiftExampleWallet/Utilities/Constants.swift +++ b/BDKSwiftExampleWallet/Utilities/Constants.swift @@ -6,17 +6,67 @@ // import Foundation +import SwiftUI struct Constants { struct Config { struct EsploraServerURLNetwork { + struct Bitcoin { + private static let blockstream = "https://blockstream.info/api" + private static let kuutamo = "https://esplora.kuutamo.cloud" + private static let mempoolspace = "https://mempool.space/api" + static let allValues = [ + blockstream, + kuutamo, + mempoolspace, + ] + } + struct Regtest { + private static let local = "http://127.0.0.1:3002" + static let allValues = [ + local + ] + } struct Signet { static let bdk = "http://signet.bitcoindevkit.net:3003/" static let mutiny = "https://mutinynet.com/api" + static let allValues = [ + bdk, + mutiny, + ] } struct Testnet { static let blockstream = "http://blockstream.info/testnet/api/" - static let mempool = "https://mempool.space/testnet/api/" + static let kuutamo = "https://esplora.testnet.kuutamo.cloud" + static let mempoolspace = "https://mempool.space/testnet/api/" + static let allValues = [ + blockstream, + kuutamo, + mempoolspace, + ] + } + } + } + enum BitcoinNetworkColor { + case bitcoin + case regtest + case signet + case testnet + + var color: Color { + switch self { + case .regtest: + return Color.green + case .signet: + return Color.yellow + case .bitcoin: + // Supposed to be `Color.black` + // ... but I'm just going to make it `Color.orange` + // ... since `Color.black` might not work well for both light+dark mode + // ... and `Color.orange` just makes more sense to me + return Color.orange + case .testnet: + return Color.red } } } diff --git a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift index 748eebe..2d9dc14 100644 --- a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift @@ -14,11 +14,80 @@ import SwiftUI // Feature or Bug? class OnboardingViewModel: ObservableObject { @AppStorage("isOnboarding") var isOnboarding: Bool? + @Published var networkColor = Color.gray + @Published var onboardingViewError: BdkError? let bdkClient: BDKClient @Published var words: String = "" + @Published var selectedNetwork: Network = .testnet { + didSet { + do { + let networkString = selectedNetwork.description + try KeyClient.live.saveNetwork(networkString) + selectedURL = availableURLs.first ?? "" + try KeyClient.live.saveEsploraURL(selectedURL) + } catch { + DispatchQueue.main.async { + self.onboardingViewError = .InvalidNetwork(message: "Error Selecting Network") + } + } + } + } + @Published var selectedURL: String = "" { + didSet { + do { + try KeyClient.live.saveEsploraURL(selectedURL) + } catch { + DispatchQueue.main.async { + self.onboardingViewError = .Esplora(message: "Error Selecting Esplora") + } + } + } + } init(bdkClient: BDKClient = .live) { self.bdkClient = bdkClient + do { + if let networkString = try KeyClient.live.getNetwork() { + self.selectedNetwork = Network(stringValue: networkString) ?? .testnet + } else { + self.selectedNetwork = .testnet + } + if let esploraURL = try KeyClient.live.getEsploraURL() { + self.selectedURL = esploraURL + } else { + self.selectedURL = availableURLs.first ?? "" + } + } catch { + DispatchQueue.main.async { + self.onboardingViewError = .Esplora(message: "Error Selecting Esplora") + } + } + } + + var availableURLs: [String] { + switch selectedNetwork { + case .bitcoin: + return Constants.Config.EsploraServerURLNetwork.Bitcoin.allValues + case .testnet: + return Constants.Config.EsploraServerURLNetwork.Testnet.allValues + case .regtest: + return Constants.Config.EsploraServerURLNetwork.Regtest.allValues + case .signet: + return Constants.Config.EsploraServerURLNetwork.Signet.allValues + } + } + + var buttonColor: Color { + switch selectedNetwork { + case .bitcoin: + return Constants.BitcoinNetworkColor.bitcoin.color + case .testnet: + return Constants.BitcoinNetworkColor.testnet.color + case .signet: + return Constants.BitcoinNetworkColor.signet.color + case .regtest: + return Constants.BitcoinNetworkColor.regtest.color + } } func createWallet() { diff --git a/BDKSwiftExampleWallet/View Model/SettingsViewModel.swift b/BDKSwiftExampleWallet/View Model/SettingsViewModel.swift index fc27be4..affdfbf 100644 --- a/BDKSwiftExampleWallet/View Model/SettingsViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/SettingsViewModel.swift @@ -11,13 +11,87 @@ import SwiftUI class SettingsViewModel: ObservableObject { let bdkClient: BDKClient + let keyClient: KeyClient @AppStorage("isOnboarding") var isOnboarding: Bool = true @Published var settingsError: BdkError? + @Published var network: String? + @Published var esploraURL: String? + // @Published var selectedNetwork: Network = .testnet { + // didSet { + // do { + // let networkString = selectedNetwork.description + // try KeyClient.live.saveNetwork(networkString) + // } catch { + // DispatchQueue.main.async { + // self.settingsError = .InvalidNetwork(message: "Error Selecting Network") + // } + // } + // } + // } + // @Published var selectedNetwork: Network = .testnet { + // didSet { + // do { + // let networkString = selectedNetwork.description + // try KeyClient.live.saveNetwork(networkString) + // selectedURL = availableURLs.first ?? "" + // //try KeyClient.live.saveEsploraURL(selectedURL) + // } catch { + // DispatchQueue.main.async { + // self.settingsError = .InvalidNetwork(message: "Error Selecting Network") + // } + // } + // } + // } + // @Published var selectedURL: String = "" { + // didSet { + // do { + // //try KeyClient.live.saveEsploraURL(selectedURL) + // } catch { + // DispatchQueue.main.async { + // self.settingsError = .Esplora(message: "Error Selecting Esplora") + // } + // } + // } + // } - init(bdkClient: BDKClient = .live) { + init( + bdkClient: BDKClient = .live, + keyClient: KeyClient = .live + ) { self.bdkClient = bdkClient + self.keyClient = keyClient + // do { + // if let networkString = try KeyClient.live.getNetwork() { + // self.selectedNetwork = Network(stringValue: networkString) ?? .testnet + // } else { + // self.selectedNetwork = .testnet + // } + // if let esploraURL = try KeyClient.live.getEsploraURL() { + // self.selectedURL = esploraURL + // } else { + // self.selectedURL = availableURLs.first ?? "" + // } + // } catch { + // DispatchQueue.main.async { + // self.settingsError = .Esplora(message: "Error Selecting Esplora") + // } + // } + } + // var availableURLs: [String] { + // switch selectedNetwork { + // case .bitcoin: + // return Constants.Config.EsploraServerURLNetwork.Bitcoin.allValues + // case .testnet: + // return Constants.Config.EsploraServerURLNetwork.Testnet.allValues + // case .regtest: + // return Constants.Config.EsploraServerURLNetwork.Regtest.allValues + // case .signet: + // return Constants.Config.EsploraServerURLNetwork.Signet.allValues + // } + // } + func delete() { do { try bdkClient.deleteWallet() @@ -32,4 +106,32 @@ class SettingsViewModel: ObservableObject { } } } + + func getNetwork() { + do { + self.network = try keyClient.getNetwork() + } catch _ as BdkError { + DispatchQueue.main.async { + self.settingsError = BdkError.Generic(message: "Could not get network") + } + } catch { + DispatchQueue.main.async { + self.settingsError = BdkError.Generic(message: "Could not get network") + } + } + } + + func getEsploraUrl() { + do { + self.esploraURL = try keyClient.getEsploraURL() + } catch _ as BdkError { + DispatchQueue.main.async { + self.settingsError = BdkError.Generic(message: "Could not get esplora") + } + } catch { + DispatchQueue.main.async { + self.settingsError = BdkError.Generic(message: "Could not get esplora") + } + } + } } diff --git a/BDKSwiftExampleWallet/View/OnboardingView.swift b/BDKSwiftExampleWallet/View/OnboardingView.swift index 247f502..6cc082d 100644 --- a/BDKSwiftExampleWallet/View/OnboardingView.swift +++ b/BDKSwiftExampleWallet/View/OnboardingView.swift @@ -5,6 +5,7 @@ // Created by Matthew Ramsden on 5/23/23. // +import BitcoinDevKit import BitcoinUI import SwiftUI @@ -37,6 +38,50 @@ struct OnboardingView: View { .multilineTextAlignment(.center) } + VStack { + + Text("Choose your Network.") + .textStyle(BitcoinBody4()) + .multilineTextAlignment(.center) + + VStack { + Picker( + "Network", + selection: $viewModel.selectedNetwork + ) { + Text("Bitcoin").tag(Network.bitcoin) + Text("Testnet").tag(Network.testnet) + Text("Signet").tag(Network.signet) + Text("Regtest").tag(Network.regtest) + } + .pickerStyle(.automatic) + .tint(viewModel.buttonColor) + + Picker( + "Esplora Server", + selection: $viewModel.selectedURL + ) { + ForEach(viewModel.availableURLs, id: \.self) { url in + Text( + url.replacingOccurrences( + of: "https://", + with: "" + ).replacingOccurrences( + of: "http://", + with: "" + ) + ) + .tag(url) + } + } + .pickerStyle(.automatic) + .tint(viewModel.buttonColor) + + } + + } + .padding() + VStack(spacing: 25) { TextField("12 Word Seed Phrase (Optional)", text: $viewModel.words) .textFieldStyle(RoundedBorderTextFieldStyle()) diff --git a/BDKSwiftExampleWallet/View/SettingsView.swift b/BDKSwiftExampleWallet/View/SettingsView.swift index dfe529e..e61e5a1 100644 --- a/BDKSwiftExampleWallet/View/SettingsView.swift +++ b/BDKSwiftExampleWallet/View/SettingsView.swift @@ -5,6 +5,7 @@ // Created by Matthew Ramsden on 1/24/24. // +import BitcoinUI import SwiftUI struct SettingsView: View { @@ -19,6 +20,23 @@ struct SettingsView: View { VStack(spacing: 20.0) { + VStack { + if let network = viewModel.network, let url = viewModel.esploraURL { + Text("Network: \(network)".uppercased()).bold() + Text( + url.replacingOccurrences( + of: "https://", + with: "" + ).replacingOccurrences( + of: "http://", + with: "" + ) + ) + } + + } + .foregroundColor(.bitcoinOrange) + Text("Danger Zone") .bold() .foregroundColor(.red) @@ -48,6 +66,10 @@ struct SettingsView: View { } } + .onAppear { + viewModel.getNetwork() + viewModel.getEsploraUrl() + } }