From bf380414ee5549e7095f8fb854d163164206a262 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 10 Nov 2024 20:34:30 -0600 Subject: [PATCH 1/3] wip: verify address with coldcard q --- .../project.pbxproj | 2 + BDKSwiftExampleWallet/Info.plist | 4 +- .../View Model/Receive/ReceiveViewModel.swift | 82 ++++++++++++++++++- .../View/Receive/ReceiveView.swift | 8 ++ 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index 6e396fa7..b2f2b3da 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -862,6 +862,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BDKSwiftExampleWallet/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "BDK Wallet"; + INFOPLIST_KEY_NFCReaderUsageDescription = "We need NFC access to verify addresses."; INFOPLIST_KEY_NSCameraUsageDescription = "\"To scan QR codes\""; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -895,6 +896,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BDKSwiftExampleWallet/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "BDK Wallet"; + INFOPLIST_KEY_NFCReaderUsageDescription = "We need NFC access to verify addresses."; INFOPLIST_KEY_NSCameraUsageDescription = "\"To scan QR codes\""; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/BDKSwiftExampleWallet/Info.plist b/BDKSwiftExampleWallet/Info.plist index 0e664efa..990bd2ef 100644 --- a/BDKSwiftExampleWallet/Info.plist +++ b/BDKSwiftExampleWallet/Info.plist @@ -5,8 +5,6 @@ ITSAppUsesNonExemptEncryption NSPasteboardUsageDescription - "To allow users to copy and paste text between the app and other apps" - NSCameraUsageDescription - "To scan QR codes" + "To allow users to copy and paste text between the app and other apps" diff --git a/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift b/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift index f7354e42..54b5db7a 100644 --- a/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift @@ -6,13 +6,16 @@ // import BitcoinDevKit +import CoreNFC import Foundation import Observation @Observable -class ReceiveViewModel { +class ReceiveViewModel: NSObject, NFCNDEFReaderSessionDelegate { let bdkClient: BDKClient + private var nfcSession: NFCNDEFReaderSession? + var address: String = "" var receiveViewError: AppError? var showingReceiveViewErrorAlert = false @@ -35,3 +38,80 @@ class ReceiveViewModel { } } + +extension ReceiveViewModel { + func startNFCSession() { + guard NFCNDEFReaderSession.readingAvailable else { + receiveViewError = .generic(message: "NFC not available on this device") + showingReceiveViewErrorAlert = true + return + } + + nfcSession = NFCNDEFReaderSession( + delegate: self, + queue: nil, + invalidateAfterFirstRead: true + ) + nfcSession?.alertMessage = "Hold your iPhone near the Coldcard Q to verify the address" + nfcSession?.begin() + } + + // MARK: - NFCNDEFReaderSessionDelegate + + func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) { + guard let tag = tags.first else { return } + + session.connect(to: tag) { error in + if let error = error { + session.invalidate(errorMessage: "Connection error: \(error.localizedDescription)") + return + } + + // Send the address to verify + let ndefMessage = NFCNDEFMessage(records: [ + NFCNDEFPayload( + format: .nfcWellKnown, + type: "T".data(using: .utf8)!, + identifier: Data(), + payload: self.address.data(using: .utf8)! + ) + ]) + + tag.writeNDEF(ndefMessage) { error in + if let error = error { + session.invalidate( + errorMessage: "Failed to send address: \(error.localizedDescription)" + ) + } else { + session.alertMessage = + "Address sent to Coldcard. Please check the Coldcard screen for verification." + session.invalidate() + } + } + } + } + + func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { + DispatchQueue.main.async { + self.receiveViewError = .generic(message: error.localizedDescription) + self.showingReceiveViewErrorAlert = true + } + } + + func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) { + // Required delegate method, but no action needed when session becomes active + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { + // Check for response from Coldcard + if let message = messages.first, + let record = message.records.first, + let payload = String(data: record.payload, encoding: .utf8) + { + // Handle response if Coldcard sends one + print("Received from Coldcard: \(payload)") + } + session.invalidate() + } + +} diff --git a/BDKSwiftExampleWallet/View/Receive/ReceiveView.swift b/BDKSwiftExampleWallet/View/Receive/ReceiveView.swift index 0cecaf52..86fd3537 100644 --- a/BDKSwiftExampleWallet/View/Receive/ReceiveView.swift +++ b/BDKSwiftExampleWallet/View/Receive/ReceiveView.swift @@ -6,6 +6,7 @@ // import BitcoinUI +import CoreNFC import SwiftUI struct ReceiveView: View { @@ -51,6 +52,13 @@ struct ReceiveView: View { ) .padding() + Button { + viewModel.startNFCSession() + } label: { + Image(systemName: "wave.3.right") + .foregroundColor(.primary) + } + HStack { Button { UIPasteboard.general.string = viewModel.address From bcad702549f58e6578ffa98ec9863c9a5ffbfef6 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 13 Nov 2024 16:05:06 -0600 Subject: [PATCH 2/3] entitlement: add nfc entitlement --- BDKSwiftExampleWallet.xcodeproj/project.pbxproj | 4 ++++ .../BDKSwiftExampleWallet.entitlements | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index b2f2b3da..92ea6c8e 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -137,6 +137,7 @@ AE4984882A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BDKSwiftExampleWalletTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AE49848C2A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BDKSwiftExampleWalletTests.swift; sourceTree = ""; }; AE4984A52A1BBCB8009951E2 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + AE6474732CE559E000A270C6 /* BDKSwiftExampleWallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BDKSwiftExampleWallet.entitlements; sourceTree = ""; }; AE6715F92A9A9220005C193F /* BDKSwiftExampleWalletPriceServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BDKSwiftExampleWalletPriceServiceTests.swift; sourceTree = ""; }; AE6715FC2A9AC056005C193F /* PriceServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceServiceError.swift; sourceTree = ""; }; AE6715FE2A9AC066005C193F /* FeeServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeServiceError.swift; sourceTree = ""; }; @@ -387,6 +388,7 @@ AE49847A2A1BBBD6009951E2 /* BDKSwiftExampleWallet */ = { isa = PBXGroup; children = ( + AE6474732CE559E000A270C6 /* BDKSwiftExampleWallet.entitlements */, AE11D5EB2B784B2900D67366 /* Info.plist */, AE1C34222A424440008F807A /* App */, AE7F670A2A7451B600CED561 /* Model */, @@ -854,6 +856,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BDKSwiftExampleWallet/Preview Content\""; @@ -888,6 +891,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BDKSwiftExampleWallet/Preview Content\""; diff --git a/BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements b/BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements new file mode 100644 index 00000000..2bb4dee1 --- /dev/null +++ b/BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.nfc.readersession.formats + + TAG + + + From f85dbd5124139c3c29ee0b1b3ab0442579ddfc06 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 13 Nov 2024 19:27:33 -0600 Subject: [PATCH 3/3] successful --- .../View Model/Receive/ReceiveViewModel.swift | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift b/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift index 54b5db7a..46669a36 100644 --- a/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift @@ -50,16 +50,18 @@ extension ReceiveViewModel { nfcSession = NFCNDEFReaderSession( delegate: self, queue: nil, - invalidateAfterFirstRead: true + invalidateAfterFirstRead: false ) - nfcSession?.alertMessage = "Hold your iPhone near the Coldcard Q to verify the address" + nfcSession?.alertMessage = "Hold your iPhone near the Coldcard to verify the address" nfcSession?.begin() } // MARK: - NFCNDEFReaderSessionDelegate func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) { - guard let tag = tags.first else { return } + guard let tag = tags.first else { + return + } session.connect(to: tag) { error in if let error = error { @@ -67,31 +69,34 @@ extension ReceiveViewModel { return } - // Send the address to verify - let ndefMessage = NFCNDEFMessage(records: [ - NFCNDEFPayload( - format: .nfcWellKnown, - type: "T".data(using: .utf8)!, - identifier: Data(), - payload: self.address.data(using: .utf8)! - ) - ]) - - tag.writeNDEF(ndefMessage) { error in + let payload = NFCNDEFPayload( + format: .media, + type: "text/plain".data(using: .utf8)!, + identifier: Data(), + payload: self.address.data(using: .utf8)! + ) + + let message = NFCNDEFMessage(records: [payload]) + + tag.writeNDEF(message) { error in if let error = error { - session.invalidate( - errorMessage: "Failed to send address: \(error.localizedDescription)" - ) + session.invalidate(errorMessage: "Write failed: \(error.localizedDescription)") } else { - session.alertMessage = - "Address sent to Coldcard. Please check the Coldcard screen for verification." + session.alertMessage = "Address passed to Coldcard successfully" session.invalidate() } } + } } func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { + if let nfcError = error as? NFCReaderError, + nfcError.code == .readerSessionInvalidationErrorUserCanceled + { + return + } + DispatchQueue.main.async { self.receiveViewError = .generic(message: error.localizedDescription) self.showingReceiveViewErrorAlert = true @@ -103,13 +108,11 @@ extension ReceiveViewModel { } func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { - // Check for response from Coldcard if let message = messages.first, let record = message.records.first, let payload = String(data: record.payload, encoding: .utf8) { - // Handle response if Coldcard sends one - print("Received from Coldcard: \(payload)") + // Handle response } session.invalidate() }