Skip to content

Commit f44550a

Browse files
committed
fix(onboarding): prevent duplicate wallet creation tasks
1 parent 52b8658 commit f44550a

File tree

2 files changed

+95
-13
lines changed

2 files changed

+95
-13
lines changed

BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -128,35 +128,34 @@ class OnboardingViewModel: ObservableObject {
128128

129129
func createWallet() {
130130
// Check if wallet already exists
131-
if let existingBackup = try? bdkClient.getBackupInfo() {
132-
Task { @MainActor in
133-
self.isOnboarding = false
134-
}
131+
if (try? bdkClient.getBackupInfo()) != nil {
132+
self.isOnboarding = false
135133
return
136134
}
137135

138136
guard !isCreatingWallet else {
139137
return
140138
}
141139

142-
Task { @MainActor in
143-
self.isCreatingWallet = true
144-
}
140+
self.isCreatingWallet = true
141+
let words = self.words
142+
let isDescriptor = self.isDescriptor
143+
let isXPub = self.isXPub
145144

146145
Task {
147146
do {
148-
if WifParser.extract(from: self.words) != nil {
147+
if WifParser.extract(from: words) != nil {
149148
throw AppError.generic(
150149
message:
151150
"WIF is for sweep, not wallet creation. Open an existing wallet and use Send > Scan/Paste to sweep it."
152151
)
153152
}
154-
if self.isDescriptor {
155-
try self.bdkClient.createWalletFromDescriptor(self.words)
156-
} else if self.isXPub {
157-
try self.bdkClient.createWalletFromXPub(self.words)
153+
if isDescriptor {
154+
try self.bdkClient.createWalletFromDescriptor(words)
155+
} else if isXPub {
156+
try self.bdkClient.createWalletFromXPub(words)
158157
} else {
159-
try self.bdkClient.createWalletFromSeed(self.words)
158+
try self.bdkClient.createWalletFromSeed(words)
160159
}
161160
await MainActor.run {
162161
self.isCreatingWallet = false
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// BDKSwiftExampleWalletOnboardingViewModelTests.swift
3+
// BDKSwiftExampleWalletTests
4+
//
5+
// Created by Codex on 4/9/26.
6+
//
7+
8+
import Foundation
9+
import XCTest
10+
11+
@testable import BDKSwiftExampleWallet
12+
13+
final class BDKSwiftExampleWalletOnboardingViewModelTests: XCTestCase {
14+
private enum TestOnboardingError: Error {
15+
case noBackup
16+
}
17+
18+
private func makeBDKClient(
19+
createWalletFromSeed: @escaping (String?) throws -> Void,
20+
getBackupInfo: @escaping () throws -> BackupInfo = { throw TestOnboardingError.noBackup }
21+
) -> BDKClient {
22+
BDKClient(
23+
loadWallet: BDKClient.mock.loadWallet,
24+
deleteWallet: BDKClient.mock.deleteWallet,
25+
createWalletFromSeed: createWalletFromSeed,
26+
createWalletFromDescriptor: BDKClient.mock.createWalletFromDescriptor,
27+
createWalletFromXPub: BDKClient.mock.createWalletFromXPub,
28+
getBalance: BDKClient.mock.getBalance,
29+
transactions: BDKClient.mock.transactions,
30+
listUnspent: BDKClient.mock.listUnspent,
31+
syncWithInspector: BDKClient.mock.syncWithInspector,
32+
fullScanWithInspector: BDKClient.mock.fullScanWithInspector,
33+
getAddress: BDKClient.mock.getAddress,
34+
send: BDKClient.mock.send,
35+
sweepWif: BDKClient.mock.sweepWif,
36+
calculateFee: BDKClient.mock.calculateFee,
37+
calculateFeeRate: BDKClient.mock.calculateFeeRate,
38+
sentAndReceived: BDKClient.mock.sentAndReceived,
39+
txDetails: BDKClient.mock.txDetails,
40+
buildTransaction: BDKClient.mock.buildTransaction,
41+
getBackupInfo: getBackupInfo,
42+
needsFullScan: BDKClient.mock.needsFullScan,
43+
setNeedsFullScan: BDKClient.mock.setNeedsFullScan,
44+
getNetwork: BDKClient.mock.getNetwork,
45+
getEsploraURL: BDKClient.mock.getEsploraURL,
46+
updateNetwork: BDKClient.mock.updateNetwork,
47+
updateEsploraURL: BDKClient.mock.updateEsploraURL,
48+
getAddressType: BDKClient.mock.getAddressType,
49+
updateAddressType: BDKClient.mock.updateAddressType,
50+
getClientType: BDKClient.mock.getClientType,
51+
updateClientType: BDKClient.mock.updateClientType
52+
)
53+
}
54+
55+
@MainActor
56+
func testCreateWalletIgnoresRepeatedCallsWhileCreationInProgress() async {
57+
let started = expectation(description: "wallet creation started")
58+
let created = XCTNSNotificationExpectation(name: .walletCreated)
59+
let unblockCreation = DispatchSemaphore(value: 0)
60+
var createCallCount = 0
61+
62+
let viewModel = OnboardingViewModel(
63+
bdkClient: makeBDKClient(createWalletFromSeed: { _ in
64+
createCallCount += 1
65+
started.fulfill()
66+
unblockCreation.wait()
67+
})
68+
)
69+
viewModel.words =
70+
"abandon ability able about above absent absorb abstract absurd abuse access accident"
71+
72+
viewModel.createWallet()
73+
await fulfillment(of: [started], timeout: 1.0)
74+
viewModel.createWallet()
75+
76+
XCTAssertTrue(viewModel.isCreatingWallet)
77+
XCTAssertEqual(createCallCount, 1)
78+
79+
unblockCreation.signal()
80+
await fulfillment(of: [created], timeout: 1.0)
81+
XCTAssertFalse(viewModel.isCreatingWallet)
82+
}
83+
}

0 commit comments

Comments
 (0)