@@ -6,7 +6,6 @@ import XCTest
66
77final class AddressTypeIntegrationTests : XCTestCase {
88 let walletIndex = 0
9- let lightning = LightningService . shared
109 let settings = SettingsViewModel . shared
1110
1211 override func setUp( ) async throws {
@@ -16,6 +15,7 @@ final class AddressTypeIntegrationTests: XCTestCase {
1615 }
1716
1817 override func tearDown( ) async throws {
18+ let lightning = await MainActor . run { settings. lightningService }
1919 lightning. dumpLdkLogs ( )
2020 try Keychain . wipeEntireKeychain ( )
2121 let isRunning = await MainActor . run { lightning. status? . isRunning == true }
@@ -39,6 +39,7 @@ final class AddressTypeIntegrationTests: XCTestCase {
3939 try skipIfNotRegtest ( )
4040 let mnemonic = try StartupHandler . createNewWallet ( bip39Passphrase: nil , walletIndex: walletIndex)
4141 XCTAssertFalse ( mnemonic. isEmpty)
42+ let lightning = await MainActor . run { settings. lightningService }
4243 try await lightning. setup ( walletIndex: walletIndex)
4344 try await lightning. start ( )
4445 try await lightning. sync ( )
@@ -49,17 +50,18 @@ final class AddressTypeIntegrationTests: XCTestCase {
4950 try await setupWalletAndNode ( )
5051
5152 Logger . test ( " Getting balance for nativeSegwit " , context: " AddressTypeIntegrationTests " )
52- let balance = try await lightning . getBalanceForAddressType ( . nativeSegwit)
53+ let balance = try await settings . lightningService . getBalanceForAddressType ( . nativeSegwit)
5354 XCTAssertGreaterThanOrEqual ( balance. totalSats, 0 )
5455 Logger . test ( " Balance: \( balance. totalSats) sats " , context: " AddressTypeIntegrationTests " )
5556 }
5657
58+ @MainActor
5759 func testGetChannelFundableBalance( ) async throws {
5860 try await setupWalletAndNode ( )
5961
6062 Logger . test ( " Getting channel fundable balance " , context: " AddressTypeIntegrationTests " )
61- let ( selectedType, monitoredTypes) = LightningService . addressTypeStateFromUserDefaults ( )
62- let fundable = try await lightning . getChannelFundableBalance ( selectedType: selectedType, monitoredTypes: monitoredTypes)
63+ let ( selectedType, monitoredTypes) = Bitkit . LightningService. addressTypeStateFromUserDefaults ( )
64+ let fundable = try await settings . lightningService . getChannelFundableBalance ( selectedType: selectedType, monitoredTypes: monitoredTypes)
6365 XCTAssertGreaterThanOrEqual ( fundable, 0 )
6466 Logger . test ( " Channel fundable: \( fundable) sats " , context: " AddressTypeIntegrationTests " )
6567 }
@@ -157,8 +159,8 @@ final class AddressTypeIntegrationTests: XCTestCase {
157159
158160 settings. addressTypesToMonitor = [ . nativeSegwit, . taproot]
159161 UserDefaults . standard. synchronize ( )
160- try await lightning . restart ( )
161- try await lightning . sync ( )
162+ try await settings . lightningService . restart ( )
163+ try await settings . lightningService . sync ( )
162164
163165 Logger . test ( " Pruning empty address types after restore " , context: " AddressTypeIntegrationTests " )
164166 await settings. pruneEmptyAddressTypesAfterRestore ( )
@@ -171,4 +173,156 @@ final class AddressTypeIntegrationTests: XCTestCase {
171173 context: " AddressTypeIntegrationTests "
172174 )
173175 }
176+
177+ // MARK: - Mutex / Concurrency
178+
179+ @MainActor
180+ func testUpdateAddressTypeMutexReturnsImmediately( ) async throws {
181+ try await setupWalletAndNode ( )
182+
183+ Logger . test ( " Testing updateAddressType mutex guard " , context: " AddressTypeIntegrationTests " )
184+ // First call should succeed
185+ let success = await settings. updateAddressType ( . taproot, wallet: nil )
186+ XCTAssertTrue ( success)
187+
188+ // Same type returns true (guard: addressType == selectedAddressType)
189+ let sameTypeResult = await settings. updateAddressType ( . taproot, wallet: nil )
190+ XCTAssertTrue ( sameTypeResult, " Same type should return true immediately " )
191+ }
192+
193+ // MARK: - Channel Fundable Balance Excludes Legacy
194+
195+ @MainActor
196+ func testGetChannelFundableBalanceExcludesLegacy( ) async throws {
197+ try await setupWalletAndNode ( )
198+
199+ let blocktank = CoreService . shared. blocktank
200+
201+ // Enable legacy monitoring and switch to legacy
202+ settings. addressTypesToMonitor = [ . nativeSegwit, . legacy]
203+ UserDefaults . standard. synchronize ( )
204+ let updateSuccess = await settings. updateAddressType ( . legacy, wallet: nil )
205+ XCTAssertTrue ( updateSuccess)
206+
207+ let legacyAddress = try await settings. lightningService. newAddressForType ( . legacy)
208+ Logger . test ( " Funding legacy address: \( legacyAddress) " , context: " AddressTypeIntegrationTests " )
209+ let txId = try await blocktank. regtestDepositFunds ( address: legacyAddress, amountSat: 50000 )
210+ XCTAssertFalse ( txId. isEmpty)
211+
212+ try await blocktank. regtestMineBlocks ( 6 )
213+ try await Task . sleep ( nanoseconds: 15_000_000_000 )
214+ try await settings. lightningService. sync ( )
215+
216+ // Verify legacy has balance
217+ let legacyBalance = try await settings. lightningService. getBalanceForAddressType ( . legacy)
218+ XCTAssertGreaterThan ( legacyBalance. totalSats, 0 , " Legacy should have balance " )
219+
220+ // Channel fundable should NOT include legacy
221+ let fundable = try await settings. lightningService. getChannelFundableBalance (
222+ selectedType: . legacy,
223+ monitoredTypes: [ . nativeSegwit, . legacy]
224+ )
225+ XCTAssertEqual ( fundable, 0 , " Channel fundable should exclude legacy even when it has balance " )
226+ Logger . test ( " Channel fundable correctly excludes legacy: \( fundable) " , context: " AddressTypeIntegrationTests " )
227+ }
228+
229+ // MARK: - Disable Monitoring With Balance Fails
230+
231+ @MainActor
232+ func testSetMonitoringDisableWithBalanceFails( ) async throws {
233+ try await setupWalletAndNode ( )
234+
235+ let blocktank = CoreService . shared. blocktank
236+
237+ // Enable taproot monitoring
238+ settings. addressTypesToMonitor = [ . nativeSegwit]
239+ UserDefaults . standard. synchronize ( )
240+ let addSuccess = await settings. setMonitoring ( . taproot, enabled: true , wallet: nil )
241+ XCTAssertTrue ( addSuccess, " Adding taproot should succeed " )
242+
243+ // Fund the taproot address
244+ let taprootAddress = try await settings. lightningService. newAddressForType ( . taproot)
245+ Logger . test ( " Funding taproot address: \( taprootAddress) " , context: " AddressTypeIntegrationTests " )
246+ let txId = try await blocktank. regtestDepositFunds ( address: taprootAddress, amountSat: 50000 )
247+ XCTAssertFalse ( txId. isEmpty)
248+
249+ try await blocktank. regtestMineBlocks ( 6 )
250+ try await Task . sleep ( nanoseconds: 15_000_000_000 )
251+ try await settings. lightningService. sync ( )
252+
253+ // Verify taproot has balance
254+ let taprootBalance = try await settings. lightningService. getBalanceForAddressType ( . taproot)
255+ XCTAssertGreaterThan ( taprootBalance. totalSats, 0 , " Taproot should have balance after funding " )
256+
257+ // Attempt to disable — should fail because of balance
258+ Logger . test ( " Attempting to disable taproot monitoring with balance " , context: " AddressTypeIntegrationTests " )
259+ let disableSuccess = await settings. setMonitoring ( . taproot, enabled: false , wallet: nil )
260+ XCTAssertFalse ( disableSuccess, " Disabling type with balance should fail " )
261+ XCTAssertTrue ( settings. addressTypesToMonitor. contains ( . taproot) , " Taproot should remain monitored " )
262+ }
263+
264+ // MARK: - Prune Preserves Types With Balance
265+
266+ @MainActor
267+ func testPruneEmptyPreservesTypesWithBalance( ) async throws {
268+ try await setupWalletAndNode ( )
269+
270+ let blocktank = CoreService . shared. blocktank
271+
272+ // Enable taproot monitoring
273+ settings. addressTypesToMonitor = [ . nativeSegwit]
274+ UserDefaults . standard. synchronize ( )
275+ let addSuccess = await settings. setMonitoring ( . taproot, enabled: true , wallet: nil )
276+ XCTAssertTrue ( addSuccess)
277+
278+ // Fund the taproot address
279+ let taprootAddress = try await settings. lightningService. newAddressForType ( . taproot)
280+ Logger . test ( " Funding taproot for prune test: \( taprootAddress) " , context: " AddressTypeIntegrationTests " )
281+ let txId = try await blocktank. regtestDepositFunds ( address: taprootAddress, amountSat: 50000 )
282+ XCTAssertFalse ( txId. isEmpty)
283+
284+ try await blocktank. regtestMineBlocks ( 6 )
285+ try await Task . sleep ( nanoseconds: 15_000_000_000 )
286+ try await settings. lightningService. sync ( )
287+
288+ // Add legacy (will be empty)
289+ let addLegacy = await settings. setMonitoring ( . legacy, enabled: true , wallet: nil )
290+ XCTAssertTrue ( addLegacy)
291+ XCTAssertEqual ( settings. addressTypesToMonitor. count, 3 )
292+
293+ Logger . test ( " Pruning — should remove empty legacy but keep funded taproot " , context: " AddressTypeIntegrationTests " )
294+ await settings. pruneEmptyAddressTypesAfterRestore ( )
295+
296+ XCTAssertTrue ( settings. addressTypesToMonitor. contains ( . nativeSegwit) , " nativeSegwit should remain " )
297+ XCTAssertTrue ( settings. addressTypesToMonitor. contains ( . taproot) , " Funded taproot should remain " )
298+ XCTAssertFalse ( settings. addressTypesToMonitor. contains ( . legacy) , " Empty legacy should be pruned " )
299+ }
300+
301+ // MARK: - Address Format Verification
302+
303+ @MainActor
304+ func testNewAddressMatchesTypeFormat( ) async throws {
305+ try await setupWalletAndNode ( )
306+
307+ // Enable all types so LDK creates wallets for each
308+ settings. addressTypesToMonitor = [ . nativeSegwit]
309+ UserDefaults . standard. synchronize ( )
310+ for type in [ LDKNode . AddressType. taproot, . nestedSegwit, . legacy] {
311+ let success = await settings. setMonitoring ( type, enabled: true , wallet: nil )
312+ XCTAssertTrue ( success, " Enabling \( type. stringValue) monitoring should succeed " )
313+ }
314+
315+ let expectations : [ ( LDKNode . AddressType , String , String ) ] = [
316+ ( . legacy, " m " , " Legacy address should start with m or n on regtest " ) ,
317+ ( . nestedSegwit, " 2 " , " Nested SegWit address should start with 2 on regtest " ) ,
318+ ( . nativeSegwit, " bcrt1q " , " Native SegWit address should start with bcrt1q on regtest " ) ,
319+ ( . taproot, " bcrt1p " , " Taproot address should start with bcrt1p on regtest " ) ,
320+ ]
321+
322+ for (type, prefix, message) in expectations {
323+ let address = try await settings. lightningService. newAddressForType ( type)
324+ Logger . test ( " \( type. stringValue) address: \( address) " , context: " AddressTypeIntegrationTests " )
325+ XCTAssertTrue ( address. hasPrefix ( prefix) , " \( message) , got: \( address) " )
326+ }
327+ }
174328}
0 commit comments