diff --git a/README.md b/README.md index a3bb67c..22a3af0 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,6 @@ A [BIP78 over Nostr Payjoin](https://github.com/Kukks/BTCPayServer.BIP78/tree/no ## Limitations -- Does not create/import/recover a new wallet, you must use an existing BIP84 wallet via Bitcoin Core. -- Only works with a local node (for now). - Native segwit inputs and outputs only. - Must have a BIP39 signer that can sign for your inputs. - Tor is not currently used for nostr traffic, a VPN is recommended, your messages will not be identifiable as bitcoin transactions to the relay. @@ -51,7 +49,6 @@ A [BIP78 over Nostr Payjoin](https://github.com/Kukks/BTCPayServer.BIP78/tree/no ## TODO - Watch-only capability, add ability to export each psbt for signing and paste back in. -- Tor for Bitcoin Core node connection (currently `localhost` only). - Allow sender to add additional inputs/outputs. - Submit PR to BIP78 nostr addendum to include a `txid` message when either party broadcasts the transaction. diff --git a/UnifyWallet.xcodeproj/project.pbxproj b/UnifyWallet.xcodeproj/project.pbxproj index 4fff81a..0c716d1 100644 --- a/UnifyWallet.xcodeproj/project.pbxproj +++ b/UnifyWallet.xcodeproj/project.pbxproj @@ -922,7 +922,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = UnifyWallet/UnifyWallet.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"UnifyWallet/Preview Content\""; DEVELOPMENT_TEAM = 8JHDU5M9KD; ENABLE_HARDENED_RUNTIME = YES; @@ -932,6 +932,9 @@ "FRAMEWORK_SEARCH_PATHS[arch=*]" = "$(inherited)"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = UnifyWallet/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Unify Wallet"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSCameraUsageDescription = "To scan QR codes."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -947,7 +950,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=iphoneos*]" = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.dentonllc.UnifyWallet; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -968,7 +971,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = UnifyWallet/UnifyWallet.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"UnifyWallet/Preview Content\""; DEVELOPMENT_TEAM = 8JHDU5M9KD; ENABLE_HARDENED_RUNTIME = YES; @@ -978,6 +981,9 @@ "FRAMEWORK_SEARCH_PATHS[arch=*]" = "$(inherited)"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = UnifyWallet/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Unify Wallet"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSCameraUsageDescription = "To scan QR codes."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -992,7 +998,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.dentonllc.UnifyWallet; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -1023,6 +1029,8 @@ "FRAMEWORK_SEARCH_PATHS[arch=*]" = "$(inherited)"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "UnifyWalletmacOS-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Unify Wallet"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -1038,7 +1046,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=iphoneos*]" = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.dentonllc.UnifyWallet; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -1068,6 +1076,8 @@ "FRAMEWORK_SEARCH_PATHS[arch=*]" = "$(inherited)"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "UnifyWalletmacOS-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Unify Wallet"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -1082,7 +1092,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.dentonllc.UnifyWallet; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; diff --git a/UnifyWallet/BitcoinCore/BitcoinCoreRPC.swift b/UnifyWallet/BitcoinCore/BitcoinCoreRPC.swift index f39557c..168c489 100644 --- a/UnifyWallet/BitcoinCore/BitcoinCoreRPC.swift +++ b/UnifyWallet/BitcoinCore/BitcoinCoreRPC.swift @@ -35,9 +35,9 @@ class BitcoinCoreRPC { completion((nil, "Unable to get rpc user.")) return } - - guard let rpcPort = UserDefaults.standard.object(forKey: "rpcPort") as? String else { - completion((nil, "No rpcport specified.")) + + guard let rpcPort = credentials["rpcPort"] as? String else { + completion((nil, "Unable to get rpcPort.")) return } diff --git a/UnifyWallet/Extensions.swift b/UnifyWallet/Extensions.swift index 34cbc97..bfa3fe0 100644 --- a/UnifyWallet/Extensions.swift +++ b/UnifyWallet/Extensions.swift @@ -41,6 +41,7 @@ public extension Double { var btcBalanceWithSpaces: String { var btcBalance = abs(self.rounded(toPlaces: 8)).avoidNotation + btcBalance = btcBalance.replacingOccurrences(of: ",", with: "") if !btcBalance.contains(".") { btcBalance += ".0" } diff --git a/UnifyWallet/TorClient.swift b/UnifyWallet/TorClient.swift index 1f3febf..21fee55 100644 --- a/UnifyWallet/TorClient.swift +++ b/UnifyWallet/TorClient.swift @@ -57,10 +57,8 @@ class TorClient: NSObject, URLSessionDelegate { state = .started var proxyPort = 19786 - //var dnsPort = 19946 #if targetEnvironment(simulator) proxyPort = 19058 - dnsPort = 12349 #endif sessionConfiguration.connectionProxyDictionary = [kCFProxyTypeKey: kCFProxyTypeSOCKS, @@ -96,7 +94,7 @@ class TorClient: NSObject, URLSessionDelegate { "LearnCircuitBuildTimeout": "1", "NumEntryGuards": "8", "SafeSocks": "1", - "LongLivedPorts": "80,443", + //"LongLivedPorts": "80,443", "NumCPUs": "2", "DisableDebuggerAttachment": "1", "SafeLogging": "1" @@ -146,8 +144,6 @@ class TorClient: NSObject, URLSessionDelegate { if arguments != nil { if arguments!["PROGRESS"] != nil { let progress = Int(arguments!["PROGRESS"]!)! - //weakDelegate?.torConnProgress(progress) - //self.progress = progress self.showProgress?((progress)) if progress >= 100 { self.controller?.removeObserver(progressObs) @@ -163,23 +159,16 @@ class TorClient: NSObject, URLSessionDelegate { if established { self.state = .connected self.torConnected?((true)) - //self.torrConnFinished = true - //weakDelegate?.torConnFinished() self.controller?.removeObserver(observer) } else if self.state == .refreshing { self.state = .connected self.torConnected?((true)) - //self.torrConnFinished = true - //weakDelegate?.torConnFinished() self.controller?.removeObserver(observer) } }) } } catch { - //weakDelegate?.torConnDifficulties() - //self.torrConnFinished = false - //self.torConnDifficulties = true self.torConnected?((false)) self.state = .none } diff --git a/UnifyWallet/UnifyWalletApp.swift b/UnifyWallet/UnifyWalletApp.swift index add232b..ceacfb7 100644 --- a/UnifyWallet/UnifyWalletApp.swift +++ b/UnifyWallet/UnifyWalletApp.swift @@ -10,7 +10,6 @@ import SwiftUI @main struct UnifyWalletApp: App { - //@StateObject private var manager: DataManager = DataManager() @State private var showNotSavedAlert = false @@ -22,8 +21,6 @@ struct UnifyWalletApp: App { var body: some Scene { WindowGroup { HomeView() - //.environmentObject(manager) - //.environment(\.managedObjectContext, manager.container.viewContext) } } @@ -34,6 +31,9 @@ struct UnifyWalletApp: App { // } // DataManager.deleteAllData(entityName: "BIP39Signer") { deleted in // print("deleted signers: \(deleted)") +// } +// DataManager.deleteAllData(entityName: "TorCredentials") { deleted in +// print("deleted tor credentials: \(deleted)") // } @@ -55,17 +55,10 @@ struct UnifyWalletApp: App { return } - guard let rpcauthcreds = RPCAuth().generateCreds(username: "Unify", password: nil) else { - showNotSavedAlert = true - return - } - UserDefaults.standard.setValue("38332", forKey: "rpcPort") UserDefaults.standard.setValue("Signet", forKey: "network") - - let rpcpass = rpcauthcreds.password - - guard let encRpcPass = Crypto.encrypt(rpcpass.data(using: .utf8)!) else { + + guard let encRpcPass = Crypto.encrypt(Crypto.privKeyData()) else { showNotSavedAlert = true return } @@ -78,6 +71,7 @@ struct UnifyWalletApp: App { ] saveCreds(entityName: "RPCCredentials", dict: dict) + createDefaultTorCreds() return diff --git a/UnifyWallet/Views/Config/ConfigView.swift b/UnifyWallet/Views/Config/ConfigView.swift index e921386..7ad432b 100644 --- a/UnifyWallet/Views/Config/ConfigView.swift +++ b/UnifyWallet/Views/Config/ConfigView.swift @@ -28,18 +28,19 @@ struct ConfigView: View { @State private var encSigner = "" @State private var bitcoinCoreConnected = false @State private var tint: Color = .red - @State private var chain = UserDefaults.standard.object(forKey: "network") as? String ?? "Regtest" + @State private var chain = UserDefaults.standard.object(forKey: "network") as? String ?? "Signet" @State private var showingPassphraseAlert = false @State private var passphrase = "" @State private var passphraseConfirm = "" @State private var creatingWallet = false - @State private var torEnabled = true + @State private var torEnabled = false @State private var torProgress = 0.0 @State private var torConnected = false @State private var torDifficulties = false @State private var encryptedTorAuthKey = "" @State private var torAuthPubkey = "" @State private var rpcAddress = "127.0.0.1" + @State private var fetching = false let chains = ["Mainnet", "Signet", "Testnet", "Regtest"] @@ -52,21 +53,36 @@ struct ConfigView: View { Spacer() - Image(systemName: "circle.fill") - .foregroundColor(tint) - - if bitcoinCoreConnected { - Text("Connected") + if fetching { + Image(systemName: "circle.fill") + .foregroundColor(.orange) + + Text("Connecting...") .foregroundStyle(.secondary) + + ProgressView() + .padding(.leading) + #if os(macOS) + .scaleEffect(0.5) + #endif + } else { - Text("Disconnected") - .foregroundStyle(.secondary) - } - - Button { - setValues() - } label: { - Image(systemName: "arrow.clockwise") + Image(systemName: "circle.fill") + .foregroundColor(tint) + + if bitcoinCoreConnected { + Text("Connected") + .foregroundStyle(.secondary) + } else { + Text("Disconnected") + .foregroundStyle(.secondary) + } + + Button { + setValues() + } label: { + Image(systemName: "arrow.clockwise") + } } } @@ -156,8 +172,12 @@ struct ConfigView: View { Spacer() TextField("", text: $rpcPort) - .onChange(of: rpcPort) { +// .onChange(of: rpcPort) { +// updateRpcPort() +// } + .onSubmit { updateRpcPort() + setValues() } #if os(iOS) .keyboardType(.numberPad) @@ -200,6 +220,7 @@ struct ConfigView: View { if !torEnabled { torManager.resign() torConnected = false + torProgress = 0.0 } UserDefaults.standard.setValue(torEnabled, forKey: "torEnabled") @@ -237,7 +258,9 @@ struct ConfigView: View { } } - if torEnabled { + //if torEnabled { + // Toggle("Add Auth", isOn: $) + // check if tor is running, if it is prompt user to quit tor then try again. HStack() { Label("Privkey", systemImage: "ellipsis.rectangle.fill") .frame(maxWidth: 200, alignment: .leading) @@ -247,7 +270,7 @@ struct ConfigView: View { SecureField("", text: $encryptedTorAuthKey) Button { - print("update tor auth") + updateTorPrivkey() } label: { Image(systemName: "arrow.clockwise") } @@ -261,7 +284,7 @@ struct ConfigView: View { CopyView(item: "descriptor:x25519:" + torAuthPubkey) } - } + //} } if bitcoinCoreConnected { @@ -347,11 +370,16 @@ struct ConfigView: View { SecureField("", text: $encSigner) Button { - DataManager.deleteAllData(entityName: "Bip39Signer") { deleted in - if deleted { - self.encSigner = "" + DataManager.retrieve(entityName: "BIP39Signer") { bip39Signer in + guard let bip39Signer = bip39Signer else { return } + + DataManager.deleteAllData(entityName: "BIP39Signer") { deleted in + if deleted { + self.encSigner = "" + } } } + } label: { Image(systemName: "trash") .foregroundStyle(.red) @@ -494,10 +522,11 @@ struct ConfigView: View { private func setValues() { + fetching = true rpcWallets.removeAll() rpcWallet = "" - torEnabled = UserDefaults.standard.object(forKey: "torEnabled") as? Bool ?? false + //torEnabled = UserDefaults.standard.object(forKey: "torEnabled") as? Bool ?? false DataManager.retrieve(entityName: "TorCredentials") { torCredDict in guard let torCredDict = torCredDict else { return } @@ -541,6 +570,10 @@ struct ConfigView: View { return } + if let rpcAddress = credentials["rpcAddress"] as? String { + torEnabled = rpcAddress.hasSuffix(".onion") + } + rpcAddress = credentials["rpcAddress"] as? String ?? "127.0.0.1" rpcPort = credentials["rpcPort"] as? String ?? "38332" @@ -570,7 +603,11 @@ struct ConfigView: View { nostrRelay = UserDefaults.standard.object(forKey: "nostrRelay") as? String ?? "wss://relay.damus.io" + fetching = true + BitcoinCoreRPC.shared.btcRPC(method: .listwallets) { (response, errorDesc) in + fetching = false + guard errorDesc == nil else { bitcoinCoreError = errorDesc! //showError(desc: errorDesc!) @@ -655,17 +692,52 @@ struct ConfigView: View { return } + setValues() } } + private func updateTorPrivkey() { + KeyGen().generate { (pubkey, privkey) in + guard let encrypted = Crypto.encrypt(privkey.dataUsingUTF8StringEncoding) else { return } + DataManager.update(keyToUpdate: "encryptedPrivateKey", newValue: encrypted, entity: "TORCredentials") { encryptedPrivateKeyUpdated in + guard encryptedPrivateKeyUpdated else { + showError(desc: "Unable to update encryptedPrivateKey.") + + return + } + //setValues() + DataManager.update(keyToUpdate: "publicKey", newValue: pubkey, entity: "TORCredentials") { publicKeyUpdated in + guard publicKeyUpdated else { + showError(desc: "Unable to update publicKey.") + + return + } + setValues() + } + } + } + + } + + private func updateRpcAddress() { - DataManager.update(keyToUpdate: "rpcAddress", newValue: rpcAddress, entity: "RPCCredentials") { rpcPortUpdated in - guard rpcPortUpdated else { + DataManager.update(keyToUpdate: "rpcAddress", newValue: rpcAddress, entity: "RPCCredentials") { rpcAddressUpdated in + guard rpcAddressUpdated else { showError(desc: "Unable to update RPC address.") return } + + if rpcAddress.hasSuffix(".onion") { + torEnabled = true + } else { + torEnabled = false + torManager.resign() + torConnected = false + torProgress = 0.0 + + } } } @@ -699,7 +771,7 @@ struct ConfigView: View { let dict: [String: Any] = ["encryptedData": encSeed] - DataManager.saveEntity(entityName: "Signers", dict: dict) { saved in + DataManager.saveEntity(entityName: "BIP39Signer", dict: dict) { saved in guard saved else { showError(desc: "Unable to save the encrypted signer.") diff --git a/UnifyWallet/Views/History/HistoryView.swift b/UnifyWallet/Views/History/HistoryView.swift index addb3b0..12bbfc9 100644 --- a/UnifyWallet/Views/History/HistoryView.swift +++ b/UnifyWallet/Views/History/HistoryView.swift @@ -13,21 +13,30 @@ struct HistoryView: View { @State private var copied = false @State private var showError = false @State private var errorToShow = "" + @State private var fetching = false var body: some View { + HStack() { + Spacer() + + if fetching { + ProgressView() + #if os(macOS) + .scaleEffect(0.5) + #endif + } else { + Button() { + listTransactions() + } label: { + Image(systemName: "arrow.clockwise") + .foregroundStyle(.blue) + } + } + } Form() { List() { - HStack() { - Spacer() - - Button() { - listTransactions() - } label: { - Image(systemName: "arrow.clockwise") - .foregroundStyle(.blue) - } - } + ForEach(transactions, id: \.self) { transaction in let amount = transaction.amount.btcBalanceWithSpaces @@ -172,10 +181,13 @@ struct HistoryView: View { private func listTransactions() { + fetching = true let p = List_Transactions(["count": 100]) transactions.removeAll() BitcoinCoreRPC.shared.btcRPC(method: .listtransactions(p)) { (response, errorDesc) in + fetching = false + guard let transactions = response as? [[String: Any]] else { displayError(desc: errorDesc ?? "Unknown error listtransactions.") diff --git a/UnifyWallet/Views/Receive/Receive Child Views/InvoiceView.swift b/UnifyWallet/Views/Receive/Receive Child Views/InvoiceView.swift index 50278f1..d158545 100644 --- a/UnifyWallet/Views/Receive/Receive Child Views/InvoiceView.swift +++ b/UnifyWallet/Views/Receive/Receive Child Views/InvoiceView.swift @@ -44,56 +44,112 @@ struct InvoiceView: View, DirectMessageEncrypting { if let ourKeypair = ourKeypair { let url = "bitcoin:\(invoiceAddress)?amount=\(invoiceAmount)&pj=nostr:\(ourKeypair.publicKey.npub)" - Section("Payjoin Invoice") { - Label("Payjoin over Nostr Invoice", systemImage: "qrcode") + if outputAddress == nil && outputAmount == nil { + let standardInvoice = "bitcoin:\(invoiceAddress)?amount=\(invoiceAmount)" - QRView(url: url) - - HStack { - Text(url) - .truncationMode(.middle) - .lineLimit(1) + Section("Standard Invoice") { + Label("Standard BIP21 Invoice", systemImage: "qrcode") - Button { - #if os(macOS) - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(url, forType: .string) - #elseif os(iOS) - UIPasteboard.general.string = url - #endif - showCopiedAlert = true + QRView(url: standardInvoice) + + HStack { + Text(standardInvoice) + .truncationMode(.middle) + .lineLimit(1) - } label: { - Image(systemName: "doc.on.doc") + Button { + #if os(macOS) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(standardInvoice, forType: .string) + #elseif os(iOS) + UIPasteboard.general.string = standardInvoice + #endif + showCopiedAlert = true + + } label: { + Image(systemName: "doc.on.doc") + } } - } - - HStack() { - Label { - Text("Invoice Address") - .foregroundStyle(.secondary) - } icon: { - Image(systemName: "arrow.down.forward.circle") - .foregroundStyle(.blue) + + HStack() { + Label { + Text("Invoice Address") + .foregroundStyle(.secondary) + } icon: { + Image(systemName: "arrow.down.forward.circle") + .foregroundStyle(.blue) + } + + Text(invoiceAddress) } - Text(invoiceAddress) + HStack() { + Label { + Text("Invoice Amount") + .foregroundStyle(.secondary) + } icon: { + Image(systemName: "bitcoinsign.circle") + .foregroundStyle(.blue) + } + + Text(invoiceAmount.btcBalanceWithSpaces) + } + + Text("Share this invoice with the payee.") + .foregroundStyle(.secondary) } - - HStack() { - Label { - Text("Invoice Amount") - .foregroundStyle(.secondary) - } icon: { - Image(systemName: "bitcoinsign.circle") - .foregroundStyle(.blue) + } else { + Section("Payjoin Invoice") { + Label("Payjoin over Nostr Invoice", systemImage: "qrcode") + + QRView(url: url) + + HStack { + Text(url) + .truncationMode(.middle) + .lineLimit(1) + + Button { + #if os(macOS) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(url, forType: .string) + #elseif os(iOS) + UIPasteboard.general.string = url + #endif + showCopiedAlert = true + + } label: { + Image(systemName: "doc.on.doc") + } } - Text(invoiceAmount.btcBalanceWithSpaces) + HStack() { + Label { + Text("Invoice Address") + .foregroundStyle(.secondary) + } icon: { + Image(systemName: "arrow.down.forward.circle") + .foregroundStyle(.blue) + } + + Text(invoiceAddress) + } + + HStack() { + Label { + Text("Invoice Amount") + .foregroundStyle(.secondary) + } icon: { + Image(systemName: "bitcoinsign.circle") + .foregroundStyle(.blue) + } + + Text(invoiceAmount.btcBalanceWithSpaces) + } + + Text("Share this invoice with the payee, they will send us the original psbt which we may broadcast as is or optionally create a Payjoin proposal.") + .foregroundStyle(.secondary) } - - Text("Share this invoice with the payee, they will send us the original psbt which we may broadcast as is or optionally create a Payjoin proposal.") - .foregroundStyle(.secondary) } } @@ -358,6 +414,9 @@ struct InvoiceView: View, DirectMessageEncrypting { return } + print("invoiceAddress: \(invoiceAddress)") + // Failing for regtest address :( + let invoiceAddress = try! Address(string: invoiceAddress) var allInputsSegwit = false diff --git a/UnifyWallet/Views/Receive/Receive Child Views/ReceiveAddOutputView.swift b/UnifyWallet/Views/Receive/Receive Child Views/ReceiveAddOutputView.swift index 339fb40..47a893e 100644 --- a/UnifyWallet/Views/Receive/Receive Child Views/ReceiveAddOutputView.swift +++ b/UnifyWallet/Views/Receive/Receive Child Views/ReceiveAddOutputView.swift @@ -63,7 +63,7 @@ struct ReceiveAddOutputView: View { Text("Additional Output") } footer: { - Text("Adding an output to the transaction is what makes this a Pajoin transaction. This should be a payment to another entity or a consolidation. This output will not be shown in the invoice.") + Text("Adding an output to the transaction is what makes this a Payjoin transaction. This should be a payment to another entity, a consolidation, donation etc... This output will not be shown in the invoice.") .foregroundStyle(.secondary) } diff --git a/UnifyWallet/Views/Receive/ReceiveView.swift b/UnifyWallet/Views/Receive/ReceiveView.swift index 00686c5..e3ea9d5 100644 --- a/UnifyWallet/Views/Receive/ReceiveView.swift +++ b/UnifyWallet/Views/Receive/ReceiveView.swift @@ -18,12 +18,25 @@ struct ReceiveView: View { @State private var torProgress = 0.0 @State private var torEnabled = false @State private var showSpinner = false + @State private var balance = 0.0 var body: some View { Form() { if torProgress < 100.0 && torEnabled { - ProgressView("Tor bootstrapping \(Int(torProgress))% complete…", value: torProgress, total: 100) + HStack() { + ProgressView("Tor bootstrapping \(Int(torProgress))% complete…", value: torProgress, total: 100) + + ProgressView() + .padding(.leading) + #if os(macOS) + .scaleEffect(0.5) + #endif + } + } + + Section("Balance") { + Label(balance.btcBalanceWithSpaces, systemImage: "bitcoinsign.circle") } Section("Create Invoice") { @@ -51,9 +64,21 @@ struct ReceiveView: View { #endif .autocorrectionDisabled() + if !showSpinner { + Button { + fetchAddress() + } label: { + Image(systemName: "arrow.clockwise") + } + } + if showSpinner { ProgressView() + #if os(macOS) .scaleEffect(0.5) + #else + .padding(.leading) + #endif } } } @@ -91,15 +116,23 @@ struct ReceiveView: View { .onAppear { amount = "" address = "" + if TorClient.sharedInstance.state == .connected { + torProgress = 100.0 + } DataManager.retrieve(entityName: "RPCCredentials") { creds in guard let creds = creds else { - errDesc = "Looks like you are new here, go to Config to add the rpcauth to your bitcoin.conf and select a wallet." - showError = true + //errDesc = "Looks like you are new here, go to Config to add the rpcauth to your bitcoin.conf and select a wallet." + //showError = true return } guard let address = creds["rpcAddress"] as? String else { return } + if address.hasPrefix("rarokrtgsiwy42pcgmrp2sds") { + errDesc = "You are using Unify in demo mode! This is a great way to test the app, navigate to Config and add your own credentials to get out of demo mode." + showError = true + } + if address.hasSuffix(".onion") { torEnabled = UserDefaults.standard.object(forKey: "torEnabled") as? Bool ?? false if torEnabled && TorClient.sharedInstance.state != .connected && TorClient.sharedInstance.state != .started { @@ -149,6 +182,7 @@ struct ReceiveView: View { } self.address = address + self.showSpinner = false } } @@ -160,13 +194,14 @@ struct ReceiveView: View { BitcoinCoreRPC.shared.btcRPC(method: .listunspent(p)) { (response, errorDesc) in guard let response = response as? [[String: Any]] else { displayError(desc: errorDesc ?? "Unknown error from listunspent.") - showSpinner = false + //showSpinner = false return } guard response.count > 0 else { displayError(desc: "No utxo's.") - showSpinner = false + //showSpinner = false + balance = 0.0 return } @@ -176,10 +211,11 @@ struct ReceiveView: View { if let confs = utxo.confs, confs > 0, let solvable = utxo.solvable, solvable { utxos.append(utxo) + balance += utxo.amount! } } - showSpinner = false + //showSpinner = false if utxos.count == 0 { displayError(desc: "No spendable utxo's.") diff --git a/UnifyWallet/Views/Send/SendNavigationLinkValues.swift b/UnifyWallet/Views/Send/SendNavigationLinkValues.swift index 4aefd0f..6b752d0 100644 --- a/UnifyWallet/Views/Send/SendNavigationLinkValues.swift +++ b/UnifyWallet/Views/Send/SendNavigationLinkValues.swift @@ -19,14 +19,14 @@ enum SendNavigationLinkValues: Hashable, View { case signedProposalView(signedRawTx: String, invoice: Invoice, - ourNostrPrivKey: String, - recipientsPubkey: String, + ourNostrPrivKey: String?, + recipientsPubkey: String?, psbtProposalString: String) case broadcastView(hexstring: String, invoice: Invoice, - ourNostrPrivateKey: String, - recipientsPubkey: String) + ourNostrPrivateKey: String?, + recipientsPubkey: String?) diff --git a/UnifyWallet/Views/Send/SendView.swift b/UnifyWallet/Views/Send/SendView.swift index 2326364..7d325ef 100644 --- a/UnifyWallet/Views/Send/SendView.swift +++ b/UnifyWallet/Views/Send/SendView.swift @@ -12,16 +12,34 @@ import SwiftUICoreImage import LibWally struct SendView: View, DirectMessageEncrypting { + @State private var uploadedInvoice: Invoice? @State private var invoiceUploaded = false @State private var showUtxos = false @State private var utxos: [Utxo] = [] @State private var showError = false @State private var errorDesc = "" + @State private var fetching = true + @State private var balance = 0.0 var body: some View { Form() { + Section("Balance") { + HStack() { + Label(balance.btcBalanceWithSpaces, systemImage: "bitcoinsign.circle") + + if fetching { + ProgressView() + #if os(macOS) + .scaleEffect(0.5) + #else + .padding(.leading) + #endif + } + } + } + if !invoiceUploaded { Section("Add Invoice") { UploadInvoiceView(uploadedInvoice: $uploadedInvoice, invoiceUploaded: $invoiceUploaded) @@ -43,6 +61,8 @@ struct SendView: View, DirectMessageEncrypting { } } + + if showUtxos, let uploadedInvoice = uploadedInvoice { if utxos.count > 0 { List() { @@ -89,6 +109,8 @@ struct SendView: View, DirectMessageEncrypting { utxos.removeAll() BitcoinCoreRPC.shared.btcRPC(method: .listunspent(p)) { (response, errorDesc) in + fetching = false + guard let response = response as? [[String: Any]] else { displayError(desc: errorDesc ?? "Unknown error from listunspent.") @@ -110,6 +132,7 @@ struct SendView: View, DirectMessageEncrypting { let solvable = utxo.solvable, solvable { spendable = true utxos.append(utxo) + balance += utxo.amount! } } @@ -230,7 +253,7 @@ struct UploadInvoiceView: View { guard let image = pasteboard.image else { guard let text = pasteboard.string else { return nil } let invoice = Invoice(text) - guard let _ = invoice.address, let _ = invoice.amount, let _ = invoice.recipientsNpub else { + guard let _ = invoice.address, let _ = invoice.amount else { return nil } @@ -262,7 +285,7 @@ struct UploadInvoiceView: View { let invoice = Invoice(qrCodeText) - guard let _ = invoice.address, let _ = invoice.recipientsNpub, let _ = invoice.amount else { + guard let _ = invoice.address, let _ = invoice.amount else { return nil } diff --git a/UnifyWallet/Views/Send/Sending Child Views/BroadcastView.swift b/UnifyWallet/Views/Send/Sending Child Views/BroadcastView.swift index 572ad13..182f8e8 100644 --- a/UnifyWallet/Views/Send/Sending Child Views/BroadcastView.swift +++ b/UnifyWallet/Views/Send/Sending Child Views/BroadcastView.swift @@ -22,8 +22,8 @@ struct BroadcastView: View, DirectMessageEncrypting { let hexstring: String let invoice: Invoice - let ourNostrPrivateKey: String - let recipientsPubkey: String + let ourNostrPrivateKey: String? + let recipientsPubkey: String? var body: some View { @@ -52,23 +52,26 @@ struct BroadcastView: View, DirectMessageEncrypting { .multilineTextAlignment(.center) .padding(.horizontal, 40) - if let _ = ourKeypair, let _ = recipientsPublicKey { + //if let _ = ourKeypair, let _ = recipientsPublicKey { Button("Broadcast") { sending = true broadcast() } .buttonStyle(.borderedProminent) - } + //} } .padding() .alert(errorDesc, isPresented: $showError) { Button("OK", role: .cancel) {} } .onAppear { - guard let nostrPrivKey = PrivateKey(hex: ourNostrPrivateKey) else { return } + if let ourNostrPrivateKey = ourNostrPrivateKey, let recipientsPubkey = recipientsPubkey { + guard let nostrPrivKey = PrivateKey(hex: ourNostrPrivateKey) else { return } + + ourKeypair = Keypair(privateKey: nostrPrivKey) + recipientsPublicKey = PublicKey(hex: recipientsPubkey)! + } - ourKeypair = Keypair(privateKey: nostrPrivKey) - recipientsPublicKey = PublicKey(hex: recipientsPubkey)! } } } else if let txid = txid { @@ -79,7 +82,7 @@ struct BroadcastView: View, DirectMessageEncrypting { .frame(width: 200, height: 200.0) .aspectRatio(contentMode: .fit) - Text("Payjoin broadcast ✓") + Text("Broadcast ✓") .font(.title) .fontWeight(.bold) @@ -135,16 +138,19 @@ struct BroadcastView: View, DirectMessageEncrypting { txid = response + if let ourKeypair = ourKeypair, let recipientsPublicKey = recipientsPublicKey { + guard let encEvent = try? encrypt(content: "Payment broadcast by sender ✓", + privateKey: ourKeypair.privateKey, + publicKey: recipientsPublicKey) else { + displayError(desc: "Encrypting event failed.") + + return + } + + StreamManager.shared.writeEvent(content: encEvent, recipientNpub: invoice.recipientsNpub!, ourKeypair: ourKeypair) + + } - guard let encEvent = try? encrypt(content: "Payment broadcast by sender ✓", - privateKey: ourKeypair!.privateKey, - publicKey: recipientsPublicKey!) else { - displayError(desc: "Encrypting event failed.") - - return - } - - StreamManager.shared.writeEvent(content: encEvent, recipientNpub: invoice.recipientsNpub!, ourKeypair: ourKeypair!) } } } diff --git a/UnifyWallet/Views/Send/Sending Child Views/SendUtxoView.swift b/UnifyWallet/Views/Send/Sending Child Views/SendUtxoView.swift index df56826..0d36696 100644 --- a/UnifyWallet/Views/Send/Sending Child Views/SendUtxoView.swift +++ b/UnifyWallet/Views/Send/Sending Child Views/SendUtxoView.swift @@ -18,7 +18,6 @@ struct SendUtxoView: View, DirectMessageEncrypting { @State private var txid: String? @State private var copied = false @State private var signedPsbt: PSBT? - @State private var proposalPsbtReceived = false @State private var ourKeypair: Keypair? @State private var recipientsPubkey: PublicKey? @State private var selection = Set() @@ -47,17 +46,15 @@ struct SendUtxoView: View, DirectMessageEncrypting { .frame(alignment: .center) } else { if let signedRawTx = signedRawTx, - let signedPsbt = signedPsbt, - let ourKeypair = ourKeypair, - let recipientsPubkey = recipientsPubkey { + let signedPsbt = signedPsbt { Button("", action: {}) .onAppear { sendNavigator.path.append( SendNavigationLinkValues.signedProposalView(signedRawTx: signedRawTx, invoice: invoice, - ourNostrPrivKey: ourKeypair.privateKey.hex, - recipientsPubkey: recipientsPubkey.hex, + ourNostrPrivKey: ourKeypair?.privateKey.hex, + recipientsPubkey: recipientsPubkey?.hex, psbtProposalString: signedPsbt.description) ) } @@ -77,10 +74,17 @@ struct SendUtxoView: View, DirectMessageEncrypting { .multilineTextAlignment(.center) .padding(.horizontal, 40) - Text("The recipient may broadcast the payment as is, or respond with a payjoin proposal which will be presented upon receipt.") - .multilineTextAlignment(.center) - .padding(.horizontal, 40) - .font(.subheadline) + if let _ = invoice.recipientsNpub { + Text("The recipient may broadcast the payment as is, or respond with a payjoin proposal which will be presented upon receipt.") + .multilineTextAlignment(.center) + .padding(.horizontal, 40) + .font(.subheadline) + } else { + Text("This is a standard invoice payment, the transaction will be broadcast as is.") + .multilineTextAlignment(.center) + .padding(.horizontal, 40) + .font(.subheadline) + } Button { showingPassphraseAlert.toggle() @@ -116,7 +120,11 @@ struct SendUtxoView: View, DirectMessageEncrypting { } } } message: { - Text("Unify will create a standard transaction to pay the invoice, the recipient will optionally broadcast the payment or send us a Payjoin proposal. Your passphrase will be used to sign the invoice payment.") + if let _ = invoice.recipientsNpub { + Text("Unify will create a standard transaction to pay the invoice, the recipient will optionally broadcast the payment or send us a Payjoin proposal. Your passphrase will be used to sign the invoice payment.") + } else { + Text("This is a standard payment invoice, Unfiy will broadcast the payment as is.") + } } .padding(.top) } @@ -211,20 +219,29 @@ struct SendUtxoView: View, DirectMessageEncrypting { return } - guard let ourKeypair = Keypair() else { - showError(desc: "Could not generate keypair.") + guard let recipientsNpub = invoice.recipientsNpub else { + // Its a standard tx just present the broadcaster. + guard let signedPsbt = try? PSBT(psbt: signedPsbt, network: network) else { + showError(desc: "Unable to convert signed base64 to PSBT.") + + return + } + + self.waitingForResponse = false + self.signedPsbt = signedPsbt + self.signedRawTx = rawTx return } - self.ourKeypair = ourKeypair - - guard let recipientsNpub = invoice.recipientsNpub else { - showError(desc: "Inavlid npub.") + guard let ourKeypair = Keypair() else { + showError(desc: "Could not generate keypair.") return } + self.ourKeypair = ourKeypair + guard let recipientsPubkey = PublicKey(npub: recipientsNpub) else { showError(desc: "Inavlid public key.") @@ -506,7 +523,7 @@ struct SendUtxoView: View, DirectMessageEncrypting { } guard inputsAreSegwit, outputsAreSegwit else { - showError(desc: "Somehting not segwit.") + showError(desc: "Something not segwit.") return } @@ -538,7 +555,7 @@ struct SendUtxoView: View, DirectMessageEncrypting { self.waitingForResponse = false self.signedPsbt = signedPsbt self.signedRawTx = rawTx - self.proposalPsbtReceived = true + //self.proposalPsbtReceived = true } } } diff --git a/UnifyWallet/Views/Send/Sending Child Views/SignedProposalView.swift b/UnifyWallet/Views/Send/Sending Child Views/SignedProposalView.swift index bb8eb18..88ccf15 100644 --- a/UnifyWallet/Views/Send/Sending Child Views/SignedProposalView.swift +++ b/UnifyWallet/Views/Send/Sending Child Views/SignedProposalView.swift @@ -23,16 +23,22 @@ struct SignedProposalView: View, DirectMessageEncrypting { let signedRawTx: String let invoice: Invoice - let ourNostrPrivKey: String - let recipientsPubkey: String + let ourNostrPrivKey: String? + let recipientsPubkey: String? let psbtProposalString: String var body: some View { if let psbtProposal = psbtProposal { - Text("Payjoin Proposal") - .font(.title) - .fontWeight(.bold) + if let ourKeypair = ourKeypair, let recipientsPublicKey = recipientsPublicKey { + Text("Payjoin Proposal") + .font(.title) + .fontWeight(.bold) + } else { + Text("Signed Payment") + .font(.title) + .fontWeight(.bold) + } Form() { Section("Signed Tx") { @@ -174,10 +180,13 @@ struct SignedProposalView: View, DirectMessageEncrypting { } else { Text("loading...") .onAppear { - guard let ourNostrPrivKey = PrivateKey(hex: ourNostrPrivKey) else { return } + if let ourNostrPrivKey = ourNostrPrivKey, let recipientsPubkey = recipientsPubkey { + guard let ourNostrPrivKey = PrivateKey(hex: ourNostrPrivKey) else { return } + + ourKeypair = Keypair(privateKey: ourNostrPrivKey)! + recipientsPublicKey = PublicKey(hex: recipientsPubkey)! + } - ourKeypair = Keypair(privateKey: ourNostrPrivKey)! - recipientsPublicKey = PublicKey(hex: recipientsPubkey)! psbtProposal = try! PSBT(psbt: psbtProposalString, network: .testnet) } }