@@ -23,7 +23,10 @@ class ActivityService {
2323 // MARK: - Constants
2424
2525 /// Maximum address index to search when current address exists
26- private static let maxAddressSearchIndex : UInt32 = 100_000
26+ private static let maxAddressSearchIndex : UInt32 = 1000
27+ /// Lock to prevent concurrent address searches
28+ private let addressSearchLock = NSLock ( )
29+ private var isSearchingAddresses = false
2730
2831 // MARK: - BoostTxIds Cache
2932
@@ -124,10 +127,8 @@ class ActivityService {
124127 }
125128
126129 func isOnchainActivitySeen( txid: String ) async -> Bool {
127- if let activity = try ? await getOnchainActivityByTxId ( txid: txid) {
128- return activity. seenAt != nil
129- }
130- return false
130+ let activity = try ? await getOnchainActivityByTxId ( txid: txid)
131+ return activity? . seenAt != nil
131132 }
132133
133134 func markActivityAsSeen( id: String , seenAt: UInt64 ? = nil ) async {
@@ -348,7 +349,7 @@ class ActivityService {
348349 _ payment: PaymentDetails ,
349350 transactionDetails: BitkitCore . TransactionDetails ? = nil
350351 ) async throws {
351- guard case let . onchain( txid, _ ) = payment. kind else { return }
352+ guard case let . onchain( txid, txStatus ) = payment. kind else { return }
352353
353354 let paymentTimestamp = payment. latestUpdateTimestamp
354355
@@ -358,10 +359,15 @@ class ActivityService {
358359 existingActivity = try BitkitCore . getActivityByTxId ( txId: txid) . map { . onchain( $0) }
359360 }
360361
361- // Skip if existing activity has newer timestamp to avoid overwriting local data
362+ // Determine if confirmation status is changing
363+ let ldkConfirmed = if case . confirmed = txStatus { true } else { false }
364+
365+ // Skip if existing activity has newer timestamp, unless confirmation status is changing
362366 if let existingActivity, case let . onchain( existing) = existingActivity {
363367 let existingUpdatedAt = existing. updatedAt ?? 0
364- if existingUpdatedAt > paymentTimestamp {
368+ let confirmationStatusChanging = existing. confirmed != ldkConfirmed
369+
370+ if existingUpdatedAt > paymentTimestamp && !confirmationStatusChanging {
365371 return
366372 }
367373 }
@@ -396,6 +402,7 @@ class ActivityService {
396402 let doesExist = existingOnchain? . doesExist ?? true
397403 let seenAt = existingOnchain? . seenAt
398404
405+ // Preserve existing value if it's larger than what LDK reports
399406 let ldkValue = payment. amountSats ?? 0
400407 let value : UInt64 = if let existingValue = existingOnchain? . value, existingValue > ldkValue {
401408 existingValue
@@ -487,7 +494,7 @@ class ActivityService {
487494 }
488495 return false
489496 } ) else {
490- Logger . warn ( " Payment not found for transaction \( txid) - LDK should have updated payment store before emitting event " , context: context)
497+ Logger . warn ( " Payment not found for transaction \( txid) " , context: context)
491498 return
492499 }
493500
@@ -659,17 +666,20 @@ class ActivityService {
659666 let existingActivity = try getActivityById ( activityId: payment. id)
660667 let existingLightning : LightningActivity ? = if let existingActivity, case let . lightning( ln) = existingActivity { ln } else { nil }
661668
662- // Skip if existing activity has newer timestamp to avoid overwriting local data
663- if let existingUpdatedAt = existingLightning? . updatedAt, existingUpdatedAt > paymentTimestamp {
664- return
665- }
666-
667669 let state : BitkitCore . PaymentState = switch payment. status {
668670 case . failed: . failed
669671 case . pending: . pending
670672 case . succeeded: . succeeded
671673 }
672674
675+ // Skip if existing activity has newer timestamp, unless payment status is changing
676+ if let existing = existingLightning, let existingUpdatedAt = existing. updatedAt {
677+ let statusChanging = existing. status != state
678+ if existingUpdatedAt > paymentTimestamp && !statusChanging {
679+ return
680+ }
681+ }
682+
673683 let ln = LightningActivity (
674684 id: payment. id,
675685 txType: payment. direction == . outbound ? . sent : . received,
@@ -780,18 +790,10 @@ class ActivityService {
780790 switch sweepBalance {
781791 case let . broadcastAwaitingConfirmation( channelId, _, latestSpendingTxid, _) :
782792 if latestSpendingTxid. description == txid, let channelId {
783- Logger . info (
784- " Matched sweep tx \( txid) to channel \( channelId) via pendingSweepBalance (awaiting confirmation) " ,
785- context: " findClosedChannelForTransaction "
786- )
787793 return channelId. description
788794 }
789795 case let . awaitingThresholdConfirmations( channelId, latestSpendingTxid, _, _, _) :
790796 if latestSpendingTxid. description == txid, let channelId {
791- Logger . info (
792- " Matched sweep tx \( txid) to channel \( channelId) via pendingSweepBalance (threshold confirmations) " ,
793- context: " findClosedChannelForTransaction "
794- )
795797 return channelId. description
796798 }
797799 case . pendingBroadcast:
@@ -892,6 +894,20 @@ class ActivityService {
892894 private func findReceivingAddress( for txid: String , value: UInt64 ,
893895 transactionDetails: BitkitCore . TransactionDetails ? = nil ) async throws -> String ?
894896 {
897+ // Prevent concurrent searches that could cause infinite loops
898+ addressSearchLock. lock ( )
899+ guard !isSearchingAddresses else {
900+ addressSearchLock. unlock ( )
901+ return nil
902+ }
903+ isSearchingAddresses = true
904+ addressSearchLock. unlock ( )
905+ defer {
906+ addressSearchLock. lock ( )
907+ isSearchingAddresses = false
908+ addressSearchLock. unlock ( )
909+ }
910+
895911 let details = if let provided = transactionDetails { provided } else { await fetchTransactionDetails ( txid: txid) }
896912 guard let details else {
897913 Logger . warn ( " Transaction details not available for \( txid) " , context: " CoreService.findReceivingAddress " )
@@ -903,30 +919,21 @@ class ActivityService {
903919
904920 // Check if an address matches any transaction output
905921 func matchesTransaction( _ address: String ) -> Bool {
906- details. outputs. contains { output in
907- output. scriptpubkeyAddress == address
908- }
922+ details. outputs. contains { $0. scriptpubkeyAddress == address }
909923 }
910924
911925 // Find matching address from a list, preferring exact value match
912926 func findMatch( in addresses: [ String ] ) -> String ? {
913927 // Try exact value match first
914928 for address in addresses {
915929 for output in details. outputs {
916- if output. scriptpubkeyAddress == address,
917- output. value == value
918- {
930+ if output. scriptpubkeyAddress == address, output. value == value {
919931 return address
920932 }
921933 }
922934 }
923935 // Fallback to any address match
924- for address in addresses {
925- if matchesTransaction ( address) {
926- return address
927- }
928- }
929- return nil
936+ return addresses. first { matchesTransaction ( $0) }
930937 }
931938
932939 // First, check pre-activity metadata for addresses in the transaction
@@ -939,19 +946,20 @@ class ActivityService {
939946 return currentWalletAddress
940947 }
941948
942- // Search addresses forward in batches
943- func searchAddresses( isChange: Bool ) async throws -> String ? {
949+ // Search addresses forward in batches across all address types
950+ func searchAddresses( isChange: Bool , addressTypeString : String ) async throws -> String ? {
944951 var index : UInt32 = 0
945- var currentAddressIndex : UInt32 ? = nil
952+ var currentAddressIndex : UInt32 ?
946953 let hasCurrentAddress = !currentWalletAddress. isEmpty
947- let maxIndex : UInt32 = hasCurrentAddress ? Self . maxAddressSearchIndex : batchSize
954+ let maxIndex : UInt32 = hasCurrentAddress ? min ( Self . maxAddressSearchIndex, 500 ) : batchSize
948955
949956 while index < maxIndex {
950957 let accountAddresses = try await coreService. utility. getAccountAddresses (
951958 walletIndex: 0 ,
952959 isChange: isChange,
953960 startIndex: index,
954- count: batchSize
961+ count: batchSize,
962+ addressTypeString: addressTypeString
955963 )
956964
957965 let addresses = accountAddresses. unused. map ( \. address) + accountAddresses. used. map ( \. address)
@@ -961,7 +969,6 @@ class ActivityService {
961969 currentAddressIndex = index
962970 }
963971
964- // Check for matches
965972 if let match = findMatch ( in: addresses) {
966973 return match
967974 }
@@ -981,12 +988,27 @@ class ActivityService {
981988 return nil
982989 }
983990
991+ let selectedAddressTypeString = UserDefaults . standard. string ( forKey: " selectedAddressType " ) ?? " nativeSegwit "
992+
993+ // Search all address types, prioritizing the selected type
994+ let addressTypesToSearch : [ String ] = {
995+ var types = [ selectedAddressTypeString]
996+ for type in [ " legacy " , " nestedSegwit " , " nativeSegwit " , " taproot " ] where !types. contains ( type) {
997+ types. append ( type)
998+ }
999+ return types
1000+ } ( )
1001+
9841002 // Try receiving addresses first, then change addresses
985- if let address = try await searchAddresses ( isChange: false ) {
986- return address
1003+ for addressTypeString in addressTypesToSearch {
1004+ if let address = try await searchAddresses ( isChange: false , addressTypeString: addressTypeString) {
1005+ return address
1006+ }
9871007 }
988- if let address = try await searchAddresses ( isChange: true ) {
989- return address
1008+ for addressTypeString in addressTypesToSearch {
1009+ if let address = try await searchAddresses ( isChange: true , addressTypeString: addressTypeString) {
1010+ return address
1011+ }
9901012 }
9911013
9921014 // Fallback: return first output address
@@ -1710,7 +1732,8 @@ class UtilityService {
17101732 walletIndex: Int = 0 ,
17111733 isChange: Bool ? = nil ,
17121734 startIndex: UInt32 ? = nil ,
1713- count: UInt32 ? = nil
1735+ count: UInt32 ? = nil ,
1736+ addressTypeString: String ? = nil
17141737 ) async throws -> AccountAddresses {
17151738 return try await ServiceQueue . background ( . core) {
17161739 guard let mnemonic = try Keychain . loadString ( key: . bip39Mnemonic( index: walletIndex) ) else {
@@ -1719,9 +1742,26 @@ class UtilityService {
17191742
17201743 let passphrase = try Keychain . loadString ( key: . bip39Passphrase( index: walletIndex) )
17211744
1722- // Create the correct derivation path based on network
1745+ // Create the correct derivation path based on address type and network
17231746 let coinType = Env . network == . bitcoin ? " 0 " : " 1 "
1724- let derivationPath = " m/84'/ \( coinType) '/0'/0 "
1747+ let derivationPath = if let addressTypeString {
1748+ // Use specified address type
1749+ switch addressTypeString. lowercased ( ) {
1750+ case " legacy " :
1751+ " m/44'/ \( coinType) '/0'/0 " // BIP 44
1752+ case " nestedsegwit " , " nested_segwit " :
1753+ " m/49'/ \( coinType) '/0'/0 " // BIP 49
1754+ case " nativesegwit " , " native_segwit " :
1755+ " m/84'/ \( coinType) '/0'/0 " // BIP 84
1756+ case " taproot " :
1757+ " m/86'/ \( coinType) '/0'/0 " // BIP 86
1758+ default :
1759+ " m/84'/ \( coinType) '/0'/0 " // Default to native segwit
1760+ }
1761+ } else {
1762+ // Default to native segwit (BIP 84) for backward compatibility
1763+ " m/84'/ \( coinType) '/0'/0 "
1764+ }
17251765
17261766 let response = try deriveBitcoinAddresses (
17271767 mnemonicPhrase: mnemonic,
0 commit comments