diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 9a364394..0c19b157 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -550,16 +550,7 @@ final class BDKService { return backupInfo } - func send( - address: String, - amount: UInt64, - feeRate: UInt64 - ) async throws { - let psbt = try buildTransaction( - address: address, - amount: amount, - feeRate: feeRate - ) + func send(psbt: Psbt) async throws -> Txid { try await signAndBroadcast(psbt: psbt) } @@ -667,7 +658,7 @@ final class BDKService { return txBuilder } - private func signAndBroadcast(psbt: Psbt) async throws { + private func signAndBroadcast(psbt: Psbt) async throws -> Txid { guard let wallet = self.wallet else { throw WalletError.walletNotFound } let isSigned = try wallet.sign(psbt: psbt) if isSigned { @@ -684,6 +675,7 @@ final class BDKService { } let _ = try wallet.persist(persister: persister) } + return transaction.computeTxid() } else { throw WalletError.notSigned } @@ -863,7 +855,7 @@ struct BDKClient { let syncWithInspector: (SyncScriptInspector) async throws -> Void let fullScanWithInspector: (FullScanScriptInspector) async throws -> Void let getAddress: () throws -> String - let send: (String, UInt64, UInt64) async throws -> Void + let send: (Psbt) async throws -> Txid let sweepWif: (String, UInt64) async throws -> [Txid] let calculateFee: (Transaction) throws -> Amount let calculateFeeRate: (Transaction) throws -> UInt64 @@ -904,8 +896,8 @@ extension BDKClient { try await BDKService.shared.fullScanWithInspector(inspector: inspector) }, getAddress: { try BDKService.shared.getAddress() }, - send: { (address, amount, feeRate) in - try await BDKService.shared.send(address: address, amount: amount, feeRate: feeRate) + send: { psbt in + try await BDKService.shared.send(psbt: psbt) }, sweepWif: { (wif, feeRate) in try await BDKService.shared.sweepWif(wif: wif, feeRate: feeRate) @@ -971,7 +963,7 @@ extension BDKClient { syncWithInspector: { _ in }, fullScanWithInspector: { _ in }, getAddress: { "tb1pd8jmenqpe7rz2mavfdx7uc8pj7vskxv4rl6avxlqsw2u8u7d4gfs97durt" }, - send: { _, _, _ in }, + send: { _ in try Txid.fromString(hex: String.mockHashHex) }, sweepWif: { _, _ in [] }, calculateFee: { _ in Amount.mock }, calculateFeeRate: { _ in UInt64(6.15) }, diff --git a/BDKSwiftExampleWallet/View Model/Send/BuildTransactionViewModel.swift b/BDKSwiftExampleWallet/View Model/Send/BuildTransactionViewModel.swift index 454e1a97..07848434 100644 --- a/BDKSwiftExampleWallet/View Model/Send/BuildTransactionViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Send/BuildTransactionViewModel.swift @@ -13,6 +13,7 @@ import Foundation class BuildTransactionViewModel { let bdkClient: BDKClient + var broadcastTxid: Txid? var buildTransactionViewError: AppError? var calculateFee: String? var psbt: Psbt? @@ -68,9 +69,14 @@ class BuildTransactionViewModel { } } - func send(address: String, amount: UInt64, feeRate: UInt64) async { + func send() async { + guard let psbt = self.psbt else { + self.buildTransactionViewError = .generic(message: "No transaction to send") + self.showingBuildTransactionViewErrorAlert = true + return + } do { - try await bdkClient.send(address, amount, feeRate) + self.broadcastTxid = try await bdkClient.send(psbt) NotificationCenter.default.post( name: .transactionSent, object: nil diff --git a/BDKSwiftExampleWallet/View/Send/BuildTransactionView.swift b/BDKSwiftExampleWallet/View/Send/BuildTransactionView.swift index 75576911..ce4e737a 100644 --- a/BDKSwiftExampleWallet/View/Send/BuildTransactionView.swift +++ b/BDKSwiftExampleWallet/View/Send/BuildTransactionView.swift @@ -117,32 +117,25 @@ struct BuildTransactionView: View { if !isSent { Button { - if let amt = UInt64(amount) { - viewModel.buildTransactionViewError = nil - isError = false - Task { @MainActor in - await viewModel.send( - address: address, - amount: amt, - feeRate: UInt64(fee) - ) - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - if self.viewModel.buildTransactionViewError == nil { - self.isSent = true - DispatchQueue.main.asyncAfter( - deadline: .now() + 1.5 - ) { - self.navigationPath.removeLast( - self.navigationPath.count - ) - } - } else { - self.isSent = false - self.isError = true + viewModel.buildTransactionViewError = nil + isError = false + Task { @MainActor in + await viewModel.send() + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + if self.viewModel.buildTransactionViewError == nil { + self.isSent = true + DispatchQueue.main.asyncAfter( + deadline: .now() + 1.5 + ) { + self.navigationPath.removeLast( + self.navigationPath.count + ) } + } else { + self.isSent = false + self.isError = true } } - } else { } } label: { Text("Send") @@ -171,14 +164,14 @@ struct BuildTransactionView: View { VStack { Image(systemName: "checkmark") .foregroundStyle(.green) - if let transaction = viewModel.extractTransaction() { + if let txid = viewModel.broadcastTxid { HStack { - Text("\(transaction.computeTxid())") + Text("\(txid)") .lineLimit(1) .truncationMode(.middle) Spacer() Button { - UIPasteboard.general.string = "\(transaction.computeTxid())" + UIPasteboard.general.string = "\(txid)" isCopied = true showCheckmark = true DispatchQueue.main.asyncAfter(deadline: .now() + 2) { diff --git a/BDKSwiftExampleWalletTests/View Model/BDKSwiftExampleWalletSendViewModelTests.swift b/BDKSwiftExampleWalletTests/View Model/BDKSwiftExampleWalletSendViewModelTests.swift index b24dbaa1..b4b5d86a 100644 --- a/BDKSwiftExampleWalletTests/View Model/BDKSwiftExampleWalletSendViewModelTests.swift +++ b/BDKSwiftExampleWalletTests/View Model/BDKSwiftExampleWalletSendViewModelTests.swift @@ -16,7 +16,7 @@ final class BDKSwiftExampleWalletSendViewModelTests: XCTestCase { } private func makeBDKClient( - send: @escaping (String, UInt64, UInt64) async throws -> Void + send: @escaping (Psbt) async throws -> Txid ) -> BDKClient { BDKClient( loadWallet: BDKClient.mock.loadWallet, @@ -95,16 +95,17 @@ final class BDKSwiftExampleWalletSendViewModelTests: XCTestCase { @MainActor func testBuildTransactionViewModelSendPostsTransactionSentNotification() async { let viewModel = BuildTransactionViewModel( - bdkClient: makeBDKClient(send: { _, _, _ in }) + bdkClient: makeBDKClient(send: BDKClient.mock.send) ) let expectation = XCTNSNotificationExpectation(name: .transactionSent) - - await viewModel.send( + viewModel.buildTransaction( address: "tb1pxg0lakl0x4jee73f38m334qsma7mn2yv764x9an5ylht6tx8ccdsxtktrt", amount: 100_000, feeRate: 17 ) + await viewModel.send() + await fulfillment(of: [expectation], timeout: 1.0) XCTAssertNil(viewModel.buildTransactionViewError) XCTAssertFalse(viewModel.showingBuildTransactionViewErrorAlert) @@ -113,19 +114,57 @@ final class BDKSwiftExampleWalletSendViewModelTests: XCTestCase { @MainActor func testBuildTransactionViewModelSendSurfacesAsyncFailure() async { let viewModel = BuildTransactionViewModel( - bdkClient: makeBDKClient(send: { _, _, _ in + bdkClient: makeBDKClient(send: { _ in throw TestSendError.failed }) ) + viewModel.buildTransaction( + address: "tb1pxg0lakl0x4jee73f38m334qsma7mn2yv764x9an5ylht6tx8ccdsxtktrt", + amount: 100_000, + feeRate: 17 + ) + + await viewModel.send() + + XCTAssertNotNil(viewModel.buildTransactionViewError) + XCTAssertTrue(viewModel.showingBuildTransactionViewErrorAlert) + XCTAssertNil(viewModel.broadcastTxid) + } - await viewModel.send( + @MainActor + func testBuildTransactionViewModelSendBroadcastsReviewedPsbtAndStoresReturnedTxid() async throws + { + let broadcastTxid = try Txid.fromString(hex: String.mockHashHex) + var sentTxid: String? + let viewModel = BuildTransactionViewModel( + bdkClient: makeBDKClient(send: { psbt in + sentTxid = try psbt.extractTx().transactionID + return broadcastTxid + }) + ) + viewModel.buildTransaction( address: "tb1pxg0lakl0x4jee73f38m334qsma7mn2yv764x9an5ylht6tx8ccdsxtktrt", amount: 100_000, feeRate: 17 ) + let reviewedTxid = try XCTUnwrap(viewModel.psbt).extractTx().transactionID + + await viewModel.send() + + XCTAssertEqual(sentTxid, reviewedTxid) + XCTAssertEqual(viewModel.broadcastTxid.map { "\($0)" }, "\(broadcastTxid)") + XCTAssertNil(viewModel.buildTransactionViewError) + } + + @MainActor + func testBuildTransactionViewModelSendWithoutPsbtSurfacesError() async { + let viewModel = BuildTransactionViewModel(bdkClient: .mock) + + await viewModel.send() XCTAssertNotNil(viewModel.buildTransactionViewError) XCTAssertTrue(viewModel.showingBuildTransactionViewErrorAlert) + XCTAssertNil(viewModel.broadcastTxid) } }