-
Notifications
You must be signed in to change notification settings - Fork 15
feat: minimal sweep WIF QR implementation #355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4875163
9aa8d5c
e19aa2e
f27f5a6
bad4d8d
0126a06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -145,6 +145,12 @@ class OnboardingViewModel: ObservableObject { | |
|
|
||
| Task { | ||
| do { | ||
| if self.looksLikeWif(self.words) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This guard only checks whether the raw trimmed input itself looks like a WIF. In this same PR, the send flow accepts additional formats like |
||
| throw AppError.generic( | ||
| message: | ||
| "WIF is for sweep, not wallet creation. Open an existing wallet and use Send > Scan/Paste to sweep it." | ||
| ) | ||
| } | ||
| if self.isDescriptor { | ||
| try self.bdkClient.createWalletFromDescriptor(self.words) | ||
| } else if self.isXPub { | ||
|
|
@@ -162,6 +168,11 @@ class OnboardingViewModel: ObservableObject { | |
| self.isCreatingWallet = false | ||
| self.createWithPersistError = error | ||
| } | ||
| } catch let error as AppError { | ||
| DispatchQueue.main.async { | ||
| self.isCreatingWallet = false | ||
| self.onboardingViewError = error | ||
| } | ||
| } catch { | ||
| DispatchQueue.main.async { | ||
| self.isCreatingWallet = false | ||
|
|
@@ -170,4 +181,21 @@ class OnboardingViewModel: ObservableObject { | |
| } | ||
| } | ||
| } | ||
|
|
||
| private func looksLikeWif(_ value: String) -> Bool { | ||
| let token = value.trimmingCharacters(in: .whitespacesAndNewlines) | ||
|
|
||
| guard token.count == 51 || token.count == 52 else { | ||
| return false | ||
| } | ||
|
|
||
| guard let first = token.first, "5KL9c".contains(first) else { | ||
| return false | ||
| } | ||
|
|
||
| let base58Charset = CharacterSet( | ||
| charactersIn: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" | ||
| ) | ||
| return token.unicodeScalars.allSatisfy { base58Charset.contains($0) } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,9 @@ struct AddressView: View { | |
| @State var address: String = "" | ||
| @State private var isShowingAlert = false | ||
| @State private var alertMessage = "" | ||
| @State private var isSweeping = false | ||
| private let bdkClient: BDKClient = .live | ||
| private let sweepFeeRate: UInt64 = 2 | ||
| let pasteboard = UIPasteboard.general | ||
|
|
||
| var body: some View { | ||
|
|
@@ -45,7 +48,14 @@ extension AddressView { | |
| func handleScan(result: Result<ScanResult, ScanError>) { | ||
| switch result { | ||
| case .success(let result): | ||
| let scannedAddress = result.string.lowercased().replacingOccurrences( | ||
| let scannedValue = result.string.trimmingCharacters(in: .whitespacesAndNewlines) | ||
|
|
||
| if let wif = extractWif(from: scannedValue) { | ||
| sweep(wif: wif) | ||
| return | ||
| } | ||
|
|
||
| let scannedAddress = scannedValue.lowercased().replacingOccurrences( | ||
| of: "bitcoin:", | ||
| with: "" | ||
| ) | ||
|
|
@@ -76,6 +86,12 @@ extension AddressView { | |
| isShowingAlert = true | ||
| return | ||
| } | ||
|
|
||
| if let wif = extractWif(from: trimmedContent) { | ||
| sweep(wif: wif) | ||
| return | ||
| } | ||
|
|
||
| let lowercaseAddress = trimmedContent.lowercased() | ||
| address = lowercaseAddress | ||
| navigationPath.append(NavigationDestination.amount(address: address)) | ||
|
|
@@ -84,6 +100,84 @@ extension AddressView { | |
| isShowingAlert = true | ||
| } | ||
| } | ||
|
|
||
| private func sweep(wif: String) { | ||
| guard !isSweeping else { return } | ||
| isSweeping = true | ||
|
|
||
| Task { | ||
| defer { | ||
| Task { @MainActor in | ||
| isSweeping = false | ||
| } | ||
| } | ||
|
|
||
| do { | ||
| let txids = try await bdkClient.sweepWif(wif, sweepFeeRate) | ||
| let txidText = txids.map { "\($0)" }.joined(separator: ", ") | ||
|
|
||
| await MainActor.run { | ||
| alertMessage = "Sweep broadcasted: \(txidText)" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This success branch sets |
||
| isShowingAlert = true | ||
| NotificationCenter.default.post( | ||
| name: Notification.Name("TransactionSent"), | ||
| object: nil | ||
| ) | ||
| } | ||
| } catch { | ||
| await MainActor.run { | ||
| alertMessage = "Sweep failed: \(error.localizedDescription)" | ||
| isShowingAlert = true | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func extractWif(from value: String) -> String? { | ||
| var candidates = [value] | ||
|
|
||
| if let components = URLComponents(string: value), | ||
| let queryItems = components.queryItems | ||
| { | ||
| for item in queryItems { | ||
| let key = item.name.lowercased() | ||
| if key == "wif" || key == "privkey" || key == "private_key" || key == "privatekey", | ||
| let itemValue = item.value | ||
| { | ||
| candidates.append(itemValue) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for candidate in candidates { | ||
| var token = candidate.trimmingCharacters(in: .whitespacesAndNewlines) | ||
|
|
||
| if token.lowercased().hasPrefix("wif:") { | ||
| token = String(token.dropFirst(4)) | ||
| } | ||
|
|
||
| if isLikelyWif(token) { | ||
| return token | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func isLikelyWif(_ value: String) -> Bool { | ||
| guard value.count == 51 || value.count == 52 else { | ||
| return false | ||
| } | ||
|
|
||
| guard let first = value.first, "5KL9c".contains(first) else { | ||
| return false | ||
| } | ||
|
|
||
| let base58Charset = CharacterSet( | ||
| charactersIn: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" | ||
| ) | ||
| return value.unicodeScalars.allSatisfy { base58Charset.contains($0) } | ||
| } | ||
| } | ||
|
|
||
| struct CustomScannerView: View { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sweepWifintentionally probes several descriptor candidates, but once one candidate has balance, any failure infinish,sign, orbroadcastthrows out of the whole method and prevents trying the remaining candidates. Since this is a heuristic multi-descriptor sweep, per-candidate failures should probably be isolated and skipped; otherwise one unspendable or unbroadcastable candidate can block sweeping funds from the actual matching descriptor shape.