@@ -171,4 +171,156 @@ final class AddressTypeIntegrationTests: XCTestCase {
171171 context: " AddressTypeIntegrationTests "
172172 )
173173 }
174+
175+ // MARK: - Mutex / Concurrency
176+
177+ @MainActor
178+ func testUpdateAddressTypeMutexReturnsImmediately( ) async throws {
179+ try await setupWalletAndNode ( )
180+
181+ Logger . test ( " Testing updateAddressType mutex guard " , context: " AddressTypeIntegrationTests " )
182+ // First call should succeed
183+ let success = await settings. updateAddressType ( . taproot, wallet: nil )
184+ XCTAssertTrue ( success)
185+
186+ // Same type returns true (guard: addressType == selectedAddressType)
187+ let sameTypeResult = await settings. updateAddressType ( . taproot, wallet: nil )
188+ XCTAssertTrue ( sameTypeResult, " Same type should return true immediately " )
189+ }
190+
191+ // MARK: - Channel Fundable Balance Excludes Legacy
192+
193+ @MainActor
194+ func testGetChannelFundableBalanceExcludesLegacy( ) async throws {
195+ try await setupWalletAndNode ( )
196+
197+ let blocktank = CoreService . shared. blocktank
198+
199+ // Enable legacy monitoring and switch to legacy
200+ settings. addressTypesToMonitor = [ . nativeSegwit, . legacy]
201+ UserDefaults . standard. synchronize ( )
202+ let updateSuccess = await settings. updateAddressType ( . legacy, wallet: nil )
203+ XCTAssertTrue ( updateSuccess)
204+
205+ let legacyAddress = try await lightning. newAddressForType ( . legacy)
206+ Logger . test ( " Funding legacy address: \( legacyAddress) " , context: " AddressTypeIntegrationTests " )
207+ let txId = try await blocktank. regtestDepositFunds ( address: legacyAddress, amountSat: 50000 )
208+ XCTAssertFalse ( txId. isEmpty)
209+
210+ try await blocktank. regtestMineBlocks ( 6 )
211+ try await Task . sleep ( nanoseconds: 15_000_000_000 )
212+ try await lightning. sync ( )
213+
214+ // Verify legacy has balance
215+ let legacyBalance = try await lightning. getBalanceForAddressType ( . legacy)
216+ XCTAssertGreaterThan ( legacyBalance. totalSats, 0 , " Legacy should have balance " )
217+
218+ // Channel fundable should NOT include legacy
219+ let fundable = try await lightning. getChannelFundableBalance (
220+ selectedType: . legacy,
221+ monitoredTypes: [ . nativeSegwit, . legacy]
222+ )
223+ XCTAssertEqual ( fundable, 0 , " Channel fundable should exclude legacy even when it has balance " )
224+ Logger . test ( " Channel fundable correctly excludes legacy: \( fundable) " , context: " AddressTypeIntegrationTests " )
225+ }
226+
227+ // MARK: - Disable Monitoring With Balance Fails
228+
229+ @MainActor
230+ func testSetMonitoringDisableWithBalanceFails( ) async throws {
231+ try await setupWalletAndNode ( )
232+
233+ let blocktank = CoreService . shared. blocktank
234+
235+ // Enable taproot monitoring
236+ settings. addressTypesToMonitor = [ . nativeSegwit]
237+ UserDefaults . standard. synchronize ( )
238+ let addSuccess = await settings. setMonitoring ( . taproot, enabled: true , wallet: nil )
239+ XCTAssertTrue ( addSuccess, " Adding taproot should succeed " )
240+
241+ // Fund the taproot address
242+ let taprootAddress = try await lightning. newAddressForType ( . taproot)
243+ Logger . test ( " Funding taproot address: \( taprootAddress) " , context: " AddressTypeIntegrationTests " )
244+ let txId = try await blocktank. regtestDepositFunds ( address: taprootAddress, amountSat: 50000 )
245+ XCTAssertFalse ( txId. isEmpty)
246+
247+ try await blocktank. regtestMineBlocks ( 6 )
248+ try await Task . sleep ( nanoseconds: 15_000_000_000 )
249+ try await lightning. sync ( )
250+
251+ // Verify taproot has balance
252+ let taprootBalance = try await lightning. getBalanceForAddressType ( . taproot)
253+ XCTAssertGreaterThan ( taprootBalance. totalSats, 0 , " Taproot should have balance after funding " )
254+
255+ // Attempt to disable — should fail because of balance
256+ Logger . test ( " Attempting to disable taproot monitoring with balance " , context: " AddressTypeIntegrationTests " )
257+ let disableSuccess = await settings. setMonitoring ( . taproot, enabled: false , wallet: nil )
258+ XCTAssertFalse ( disableSuccess, " Disabling type with balance should fail " )
259+ XCTAssertTrue ( settings. addressTypesToMonitor. contains ( . taproot) , " Taproot should remain monitored " )
260+ }
261+
262+ // MARK: - Prune Preserves Types With Balance
263+
264+ @MainActor
265+ func testPruneEmptyPreservesTypesWithBalance( ) async throws {
266+ try await setupWalletAndNode ( )
267+
268+ let blocktank = CoreService . shared. blocktank
269+
270+ // Enable taproot monitoring
271+ settings. addressTypesToMonitor = [ . nativeSegwit]
272+ UserDefaults . standard. synchronize ( )
273+ let addSuccess = await settings. setMonitoring ( . taproot, enabled: true , wallet: nil )
274+ XCTAssertTrue ( addSuccess)
275+
276+ // Fund the taproot address
277+ let taprootAddress = try await lightning. newAddressForType ( . taproot)
278+ Logger . test ( " Funding taproot for prune test: \( taprootAddress) " , context: " AddressTypeIntegrationTests " )
279+ let txId = try await blocktank. regtestDepositFunds ( address: taprootAddress, amountSat: 50000 )
280+ XCTAssertFalse ( txId. isEmpty)
281+
282+ try await blocktank. regtestMineBlocks ( 6 )
283+ try await Task . sleep ( nanoseconds: 15_000_000_000 )
284+ try await lightning. sync ( )
285+
286+ // Add legacy (will be empty)
287+ let addLegacy = await settings. setMonitoring ( . legacy, enabled: true , wallet: nil )
288+ XCTAssertTrue ( addLegacy)
289+ XCTAssertEqual ( settings. addressTypesToMonitor. count, 3 )
290+
291+ Logger . test ( " Pruning — should remove empty legacy but keep funded taproot " , context: " AddressTypeIntegrationTests " )
292+ await settings. pruneEmptyAddressTypesAfterRestore ( )
293+
294+ XCTAssertTrue ( settings. addressTypesToMonitor. contains ( . nativeSegwit) , " nativeSegwit should remain " )
295+ XCTAssertTrue ( settings. addressTypesToMonitor. contains ( . taproot) , " Funded taproot should remain " )
296+ XCTAssertFalse ( settings. addressTypesToMonitor. contains ( . legacy) , " Empty legacy should be pruned " )
297+ }
298+
299+ // MARK: - Address Format Verification
300+
301+ @MainActor
302+ func testNewAddressMatchesTypeFormat( ) async throws {
303+ try await setupWalletAndNode ( )
304+
305+ // Enable all types so LDK creates wallets for each
306+ settings. addressTypesToMonitor = [ . nativeSegwit]
307+ UserDefaults . standard. synchronize ( )
308+ for type in [ LDKNode . AddressType. taproot, . nestedSegwit, . legacy] {
309+ let success = await settings. setMonitoring ( type, enabled: true , wallet: nil )
310+ XCTAssertTrue ( success, " Enabling \( type. stringValue) monitoring should succeed " )
311+ }
312+
313+ let expectations : [ ( LDKNode . AddressType , String , String ) ] = [
314+ ( . legacy, " m " , " Legacy address should start with m or n on regtest " ) ,
315+ ( . nestedSegwit, " 2 " , " Nested SegWit address should start with 2 on regtest " ) ,
316+ ( . nativeSegwit, " bcrt1q " , " Native SegWit address should start with bcrt1q on regtest " ) ,
317+ ( . taproot, " bcrt1p " , " Taproot address should start with bcrt1p on regtest " ) ,
318+ ]
319+
320+ for (type, prefix, message) in expectations {
321+ let address = try await lightning. newAddressForType ( type)
322+ Logger . test ( " \( type. stringValue) address: \( address) " , context: " AddressTypeIntegrationTests " )
323+ XCTAssertTrue ( address. hasPrefix ( prefix) , " \( message) , got: \( address) " )
324+ }
325+ }
174326}
0 commit comments