Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = BitkitNotification/BitkitNotification.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 177;
CURRENT_PROJECT_VERSION = 178;
DEVELOPMENT_TEAM = KYH47R284B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BitkitNotification/Info.plist;
Expand All @@ -510,7 +510,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.0.4;
MARKETING_VERSION = 2.0.5;
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.notification;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand All @@ -526,7 +526,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = BitkitNotification/BitkitNotification.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 177;
CURRENT_PROJECT_VERSION = 178;
DEVELOPMENT_TEAM = KYH47R284B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BitkitNotification/Info.plist;
Expand All @@ -538,7 +538,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.0.4;
MARKETING_VERSION = 2.0.5;
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.notification;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand Down Expand Up @@ -672,7 +672,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Bitkit/Bitkit.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 177;
CURRENT_PROJECT_VERSION = 178;
DEVELOPMENT_ASSET_PATHS = "\"Bitkit/Preview Content\"";
DEVELOPMENT_TEAM = KYH47R284B;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -697,7 +697,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 2.0.4;
MARKETING_VERSION = 2.0.5;
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand All @@ -715,7 +715,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Bitkit/Bitkit.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 177;
CURRENT_PROJECT_VERSION = 178;
DEVELOPMENT_ASSET_PATHS = "\"Bitkit/Preview Content\"";
DEVELOPMENT_TEAM = KYH47R284B;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -740,7 +740,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 2.0.4;
MARKETING_VERSION = 2.0.5;
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand Down Expand Up @@ -893,7 +893,7 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/synonymdev/vss-rust-client-ffi";
requirement = {
branch = "master";
branch = master;
kind = branch;
};
};
Expand Down Expand Up @@ -925,8 +925,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/synonymdev/ldk-node";
requirement = {
branch = main;
kind = branch;
kind = revision;
revision = d2a82a2d111e5eb84a0eec02f4754e39fea4189a;
};
};
96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Bitkit/Constants/Env.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ enum Env {
return [
.init(nodeId: "039b8b4dd1d88c2c5db374290cda397a8f5d79f312d6ea5d5bfdfc7c6ff363eae3", host: "34.65.111.104", port: 9735),
.init(nodeId: "03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b", host: "34.65.191.64", port: 9735),
.init(nodeId: "02a371038863605300d0b3fc9de0cf5ccb57728b7f8906535709a831b16e311187", host: "34.65.186.40", port: 9735),
.init(nodeId: "02a371038863605300d0b3fc9de0cf5ccb57728b7f8906535709a831b16e311187", host: "34.65.153.174", port: 9735),
]
case .signet:
return []
Expand Down
45 changes: 43 additions & 2 deletions Bitkit/Services/LightningService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,20 +306,61 @@
Logger.info("Deleted network graph cache at: \(graphPath.path)")
}

func connectToTrustedPeers() async throws {
func connectToTrustedPeers(remotePeers: [LnPeer]? = nil) async throws {
guard let node else {
throw AppError(serviceError: .nodeNotSetup)
}

let peers: [LnPeer]
let usingRemotePeers: Bool
if let remotePeers, !remotePeers.isEmpty {
Logger.info("Using \(remotePeers.count) trusted peers from Blocktank API")
peers = remotePeers
usingRemotePeers = true
} else {
Logger.warn("No remote peers available, falling back to preconfigured env peers")
peers = Env.trustedLnPeers
usingRemotePeers = false
}

try await ServiceQueue.background(.ldk) {
for peer in Env.trustedLnPeers {
for peer in peers {
do {
try node.connect(nodeId: peer.nodeId, address: peer.address, persist: true)
Logger.info("Connected to trusted peer: \(peer.nodeId)")
} catch {
Logger.error(error, context: "Peer: \(peer.nodeId)")
}
}

if usingRemotePeers {
self.verifyTrustedPeersOrFallback(node: node, trustedPeers: peers)
}
}
}

private func verifyTrustedPeersOrFallback(node: Node, trustedPeers: [LnPeer]) {
let connectedPeerIds = Set(node.listPeers().filter(\.isConnected).map(\.nodeId))
let trustedConnected = trustedPeers.filter { connectedPeerIds.contains($0.nodeId) }.count
let trustedPeerIds = Set(trustedPeers.map(\.nodeId))

if trustedConnected == 0, !trustedPeers.isEmpty {
let fallbackPeers = Env.trustedLnPeers.filter { !trustedPeerIds.contains($0.nodeId) }
if fallbackPeers.isEmpty {
Logger.warn("No trusted peers connected. All preconfigured env peers overlap with API peers (not retrying with stale addresses).")
} else {
Logger.warn("No trusted peers connected, trying \(fallbackPeers.count) preconfigured env peer(s) not in API list")
for peer in fallbackPeers {
do {
try node.connect(nodeId: peer.nodeId, address: peer.address, persist: true)
Logger.info("Connected to fallback peer: \(peer.nodeId)")
} catch {
Logger.error(error, context: "Fallback peer: \(peer.nodeId)")
}
}
}
} else {
Logger.info("Connected to \(trustedConnected)/\(trustedPeers.count) trusted peers")
}
}

Expand Down Expand Up @@ -541,7 +582,7 @@
}

func closeChannel(_ channel: ChannelDetails, force: Bool = false, forceCloseReason: String? = nil) async throws {
guard let node else {

Check warning on line 585 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

value 'node' was defined but never used; consider replacing with boolean test

Check warning on line 585 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

value 'node' was defined but never used; consider replacing with boolean test
throw AppError(serviceError: .nodeNotStarted)
}

Expand Down Expand Up @@ -916,7 +957,7 @@
onEvent?(event)

switch event {
case let .paymentSuccessful(paymentId, paymentHash, paymentPreimage, feePaidMsat):

Check warning on line 960 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'paymentPreimage' was never used; consider replacing with '_' or removing it
Logger.info("✅ Payment successful: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash) feePaidMsat: \(feePaidMsat ?? 0)")
Task {
let hash = paymentId ?? paymentHash
Expand All @@ -941,7 +982,7 @@
Logger.warn("No paymentId or paymentHash available for failed payment", context: "LightningService")
}
}
case let .paymentReceived(paymentId, paymentHash, amountMsat, feePaidMsat):

Check warning on line 985 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'feePaidMsat' was never used; consider replacing with '_' or removing it
Logger.info("🤑 Payment received: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash) amountMsat: \(amountMsat)")
Task {
let hash = paymentId ?? paymentHash
Expand All @@ -951,7 +992,7 @@
Logger.error("Failed to handle payment received for \(hash): \(error)", context: "LightningService")
}
}
case let .paymentClaimable(paymentId, paymentHash, claimableAmountMsat, claimDeadline, customRecords):

Check warning on line 995 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'customRecords' was never used; consider replacing with '_' or removing it

Check warning on line 995 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'claimDeadline' was never used; consider replacing with '_' or removing it
Logger.info(
"🫰 Payment claimable: paymentId: \(paymentId) paymentHash: \(paymentHash) claimableAmountMsat: \(claimableAmountMsat)"
)
Expand Down Expand Up @@ -980,7 +1021,7 @@

if let channel {
await registerClosedChannel(channel: channel, reason: reasonString)
await MainActor.run {

Check warning on line 1024 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

result of call to 'run(resultType:body:)' is unused

Check warning on line 1024 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

result of call to 'run(resultType:body:)' is unused
channelCache.removeValue(forKey: channelIdString)
}
} else {
Expand All @@ -1003,7 +1044,7 @@
Logger.error("Failed to handle transaction received for \(txid): \(error)", context: "LightningService")
}
}
case let .onchainTransactionConfirmed(txid, blockHash, blockHeight, confirmationTime, details):

Check warning on line 1047 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'confirmationTime' was never used; consider replacing with '_' or removing it

Check warning on line 1047 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'blockHash' was never used; consider replacing with '_' or removing it
Logger.info("✅ Onchain transaction confirmed: txid=\(txid) blockHeight=\(blockHeight) amountSats=\(details.amountSats)")
Task {
do {
Expand Down Expand Up @@ -1057,7 +1098,7 @@

// MARK: Balance Events

case let .balanceChanged(oldSpendableOnchain, newSpendableOnchain, oldTotalOnchain, newTotalOnchain, oldLightning, newLightning):

Check warning on line 1101 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'newTotalOnchain' was never used; consider replacing with '_' or removing it

Check warning on line 1101 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'oldTotalOnchain' was never used; consider replacing with '_' or removing it
Logger
.info("💰 Balance changed: onchain=\(oldSpendableOnchain)->\(newSpendableOnchain) lightning=\(oldLightning)->\(newLightning)")

Expand Down
60 changes: 55 additions & 5 deletions Bitkit/ViewModels/WalletViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ class WalletViewModel: ObservableObject {
private let transferService: TransferService
private let sheetViewModel: SheetViewModel

enum BlocktankPeerSimulation: String, CaseIterable {
case none = "None"
case apiFailure = "API Failure"
case unreachablePeers = "Unreachable Peers"
}

static var peerSimulation: BlocktankPeerSimulation = .none

@Published var isRestoringWallet = false
@Published var balanceInTransferToSavings: Int = 0
@Published var balanceInTransferToSpending: Int = 0
Expand Down Expand Up @@ -205,11 +213,7 @@ class WalletViewModel: ObservableObject {

syncState()

do {
try await lightningService.connectToTrustedPeers()
} catch {
Logger.error("Failed to connect to trusted peers")
}
await reconnectTrustedPeers()

// Migration only: fetch peers from remote backup (once) and persist in ldk-node
let peerUris = await MigrationsService.shared.tryFetchMigrationPeersFromBackup(walletIndex: walletIndex)
Expand All @@ -232,6 +236,52 @@ class WalletViewModel: ObservableObject {
}
}

private func fetchTrustedPeersFromBlocktank() async -> [LnPeer]? {
switch Self.peerSimulation {
case .apiFailure:
Logger.warn("⚠️ [DEBUG] Simulating Blocktank API failure — returning nil")
return nil
case .unreachablePeers:
Logger.warn("⚠️ [DEBUG] Simulating unreachable API peers")
return [
LnPeer(nodeId: "000000000000000000000000000000000000000000000000000000000000000001",
host: "192.0.2.1", port: 9735),
]
case .none:
break
}


var info: IBtInfo?
do {
info = try await coreService.blocktank.info(refresh: true)
} catch {
Logger.warn("Blocktank API refresh failed, trying cache: \(error)")
}
if info == nil {
info = try? await coreService.blocktank.info(refresh: false)
}
guard let nodes = info?.nodes, !nodes.isEmpty else { return nil }
let peers = nodes.compactMap { node -> LnPeer? in
guard let connString = node.connectionStrings.first else { return nil }
let address = connString.contains("@") ? String(connString.split(separator: "@").last ?? "") : connString
let parts = address.split(separator: ":")
guard parts.count == 2, let port = UInt16(parts[1]) else { return nil }
return LnPeer(nodeId: node.pubkey, host: String(parts[0]), port: port)
}
Logger.info("Fetched \(peers.count) trusted peers from Blocktank API")
return peers.isEmpty ? nil : peers
}

func reconnectTrustedPeers() async {
do {
let remotePeers = await fetchTrustedPeersFromBlocktank()
try await lightningService.connectToTrustedPeers(remotePeers: remotePeers)
} catch {
Logger.error("Failed to connect to trusted peers")
}
}

func stopLightningNode(clearEventCallback: Bool = false) async throws {
nodeLifecycleState = .stopping
try await lightningService.stop(clearEventCallback: clearEventCallback)
Expand Down
16 changes: 16 additions & 0 deletions Bitkit/Views/Settings/LdkDebugScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ struct LdkDebugScreen: View {
}
}
}

// Peer Simulation
VStack(alignment: .leading, spacing: 8) {
CaptionMText("Peer Simulation")

Picker("Peer Simulation", selection: Binding(
get: { WalletViewModel.peerSimulation },
set: { WalletViewModel.peerSimulation = $0 }
)) {
ForEach(WalletViewModel.BlocktankPeerSimulation.allCases, id: \.self) { mode in
Text(mode.rawValue).tag(mode)
}
}
.pickerStyle(.segmented)
}
}
}
}
Expand Down Expand Up @@ -109,6 +124,7 @@ struct LdkDebugScreen: View {
isRestartingNode = true
let lightningService = LightningService.shared
try await lightningService.restart()
await wallet.reconnectTrustedPeers()
app.toast(type: .success, title: "Node Restarted", description: "Node restarted successfully")
} catch {
Logger.error("Failed to restart node: \(error)")
Expand Down
Loading