Skip to content

Commit 8cfde7b

Browse files
committed
feat: support all address types
1 parent 26418c6 commit 8cfde7b

14 files changed

Lines changed: 815 additions & 89 deletions

File tree

Bitkit.xcodeproj/project.pbxproj

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,27 +61,6 @@
6161
};
6262
/* End PBXCopyFilesBuildPhase section */
6363

64-
/* Begin PBXShellScriptBuildPhase section */
65-
96EMBED0012026012000FRAME /* Remove Static Framework Stubs */ = {
66-
isa = PBXShellScriptBuildPhase;
67-
buildActionMask = 2147483647;
68-
files = (
69-
);
70-
inputFileListPaths = (
71-
);
72-
inputPaths = (
73-
);
74-
name = "Remove Static Framework Stubs";
75-
outputFileListPaths = (
76-
);
77-
outputPaths = (
78-
);
79-
runOnlyForDeploymentPostprocessing = 0;
80-
shellPath = /bin/sh;
81-
shellScript = "# Remove static framework stubs from app bundle\\n# LDKNodeFFI is a static library - its code is linked into the main executable.\\n# The empty framework structure causes iOS install errors.\\nFRAMEWORK_PATH=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Frameworks/LDKNodeFFI.framework\"\\n\\nif [ -d \"$FRAMEWORK_PATH\" ]; then\\n echo \"Removing LDKNodeFFI static framework stub...\"\\n rm -rf \"$FRAMEWORK_PATH\"\\n echo \"Done.\"\\nfi\\n";
82-
};
83-
/* End PBXShellScriptBuildPhase section */
84-
8564
/* Begin PBXFileReference section */
8665
961058DC2C355B5500E1F1D8 /* BitkitNotification.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = BitkitNotification.appex; sourceTree = BUILT_PRODUCTS_DIR; };
8766
96FE1F612C2DE6AA006D0C8B /* Bitkit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bitkit.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -443,6 +422,27 @@
443422
};
444423
/* End PBXResourcesBuildPhase section */
445424

425+
/* Begin PBXShellScriptBuildPhase section */
426+
96EMBED0012026012000FRAME /* Remove Static Framework Stubs */ = {
427+
isa = PBXShellScriptBuildPhase;
428+
buildActionMask = 2147483647;
429+
files = (
430+
);
431+
inputFileListPaths = (
432+
);
433+
inputPaths = (
434+
);
435+
name = "Remove Static Framework Stubs";
436+
outputFileListPaths = (
437+
);
438+
outputPaths = (
439+
);
440+
runOnlyForDeploymentPostprocessing = 0;
441+
shellPath = /bin/sh;
442+
shellScript = "# Remove static framework stubs from app bundle\\n# LDKNodeFFI is a static library - its code is linked into the main executable.\\n# The empty framework structure causes iOS install errors.\\nFRAMEWORK_PATH=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Frameworks/LDKNodeFFI.framework\"\\n\\nif [ -d \"$FRAMEWORK_PATH\" ]; then\\n echo \"Removing LDKNodeFFI static framework stub...\"\\n rm -rf \"$FRAMEWORK_PATH\"\\n echo \"Done.\"\\nfi\\n";
443+
};
444+
/* End PBXShellScriptBuildPhase section */
445+
446446
/* Begin PBXSourcesBuildPhase section */
447447
961058D82C355B5500E1F1D8 /* Sources */ = {
448448
isa = PBXSourcesBuildPhase;
@@ -925,8 +925,8 @@
925925
isa = XCRemoteSwiftPackageReference;
926926
repositoryURL = "https://github.com/synonymdev/ldk-node";
927927
requirement = {
928-
branch = main;
929-
kind = branch;
928+
kind = revision;
929+
revision = 2281589d699cb6f821f1ad720435c8110cf1fa7c;
930930
};
931931
};
932932
96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */ = {

Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Bitkit/MainNavView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ struct MainNavView: View {
399399

400400
// Advanced settings
401401
case .coinSelection: CoinSelectionSettingsView()
402+
case .addressTypePreference: AddressTypePreferenceView()
402403
case .connections: LightningConnectionsView()
403404
case let .connectionDetail(channelId): LightningConnectionDetailView(channelId: channelId)
404405
case let .closeConnection(channel: channel): CloseConnectionConfirmation(channel: channel)

Bitkit/Services/CoreService.swift

Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)