Skip to content
Closed
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
26 changes: 13 additions & 13 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = BitkitWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 192;
CURRENT_PROJECT_VERSION = 193;
DEVELOPMENT_TEAM = KYH47R284B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BitkitWidget/Info.plist;
Expand All @@ -691,7 +691,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.1;
MARKETING_VERSION = 2.3.2;
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand All @@ -712,7 +712,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = BitkitWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 192;
CURRENT_PROJECT_VERSION = 193;
DEVELOPMENT_TEAM = KYH47R284B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BitkitWidget/Info.plist;
Expand All @@ -724,7 +724,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.1;
MARKETING_VERSION = 2.3.2;
PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand All @@ -744,7 +744,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = BitkitNotification/BitkitNotification.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 192;
CURRENT_PROJECT_VERSION = 193;
DEVELOPMENT_TEAM = KYH47R284B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BitkitNotification/Info.plist;
Expand All @@ -756,7 +756,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.1;
MARKETING_VERSION = 2.3.2;
OTHER_LDFLAGS = (
"-framework",
CoreBluetooth,
Expand All @@ -776,7 +776,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = BitkitNotification/BitkitNotification.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 192;
CURRENT_PROJECT_VERSION = 193;
DEVELOPMENT_TEAM = KYH47R284B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BitkitNotification/Info.plist;
Expand All @@ -788,7 +788,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.1;
MARKETING_VERSION = 2.3.2;
OTHER_LDFLAGS = (
"-framework",
CoreBluetooth,
Expand Down Expand Up @@ -926,7 +926,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Bitkit/Bitkit.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 192;
CURRENT_PROJECT_VERSION = 193;
DEVELOPMENT_ASSET_PATHS = "\"Bitkit/Preview Content\"";
DEVELOPMENT_TEAM = KYH47R284B;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -953,7 +953,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.3.1;
MARKETING_VERSION = 2.3.2;
OTHER_LDFLAGS = (
"-framework",
CoreBluetooth,
Expand All @@ -975,7 +975,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Bitkit/Bitkit.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 192;
CURRENT_PROJECT_VERSION = 193;
DEVELOPMENT_ASSET_PATHS = "\"Bitkit/Preview Content\"";
DEVELOPMENT_TEAM = KYH47R284B;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -1002,7 +1002,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.3.1;
MARKETING_VERSION = 2.3.2;
OTHER_LDFLAGS = (
"-framework",
CoreBluetooth,
Expand Down Expand Up @@ -1193,7 +1193,7 @@
repositoryURL = "https://github.com/synonymdev/ldk-node";
requirement = {
kind = exactVersion;
version = "0.7.0-rc.39";
version = "0.7.0-rc.53";
};
};
96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */ = {
Expand Down

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

49 changes: 45 additions & 4 deletions Bitkit/Services/LightningService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ class LightningService {

static var shared = LightningService()

private static let scoringBasePenaltyMsat: UInt64 = 50000
private static let scoringLiquidityPenaltyMultiplierMsat: UInt64 = 10000
private static let scoringLiquidityPenaltyAmountMultiplierMsat: UInt64 = 10000
private static let scoringHistoricalLiquidityPenaltyAmountMultiplierMsat: UInt64 = 20000
private static let scoringConsideredImpossiblePenaltyMsat: UInt64 = 1_000_000_000_000
private static let scoringProbingDiversityPenaltyMsat: UInt64 = 60000

private static let defaultScoringFeeParameters = ScoringFeeParameters(
basePenaltyMsat: 1024,
basePenaltyAmountMultiplierMsat: 131_072,
liquidityPenaltyMultiplierMsat: 0,
liquidityPenaltyAmountMultiplierMsat: 0,
historicalLiquidityPenaltyMultiplierMsat: 10000,
historicalLiquidityPenaltyAmountMultiplierMsat: 1250,
antiProbingPenaltyMsat: 250,
consideredImpossiblePenaltyMsat: 100_000_000_000,
linearSuccessProbability: false,
probingDiversityPenaltyMsat: 0
)

private init() {}

/// Flag and lock to prevent concurrent setup calls
Expand Down Expand Up @@ -107,6 +127,8 @@ class LightningService {
builder.setPathfindingScoresSource(url: scorerUrl)
}

builder.setScoringFeeParams(params: Self.scoringFeeParameters(config: config))

// Configure gossip source from current settings
configureGossipSource(builder: builder, rgsServerUrl: rgsServerUrl)

Expand Down Expand Up @@ -1245,12 +1267,14 @@ extension LightningService {
Logger.info(
"🫰 Payment claimable: paymentId: \(paymentId) paymentHash: \(paymentHash) claimableAmountMsat: \(claimableAmountMsat)"
)
case let .probeSuccessful(paymentId, paymentHash):
Logger.info("🤑 Probe successful: paymentId: \(paymentId) paymentHash: \(paymentHash)")
case let .probeFailed(paymentId, paymentHash, shortChannelId):
case let .probeSuccessful(paymentId, paymentHash, routeFeeMsat):
let routeFeeText = routeFeeMsat.map(String.init) ?? "unknown"
Logger.info("🤑 Probe successful: paymentId: \(paymentId) paymentHash: \(paymentHash) routeFeeMsat: \(routeFeeText)")
case let .probeFailed(paymentId, paymentHash, shortChannelId, routeFeeMsat):
let routeFeeText = routeFeeMsat.map(String.init) ?? "unknown"
Logger
.info(
"❌ Probe failed: paymentId: \(paymentId) paymentHash: \(paymentHash) shortChannelId: \(String(describing: shortChannelId))"
"❌ Probe failed: paymentId: \(paymentId) paymentHash: \(paymentHash) shortChannelId: \(String(describing: shortChannelId)) routeFeeMsat: \(routeFeeText)"
)
// Payment claimable doesn't need activity update - it's still pending
// The payment will be updated when it succeeds or fails via paymentSuccessful/paymentFailed events
Expand Down Expand Up @@ -1569,6 +1593,23 @@ extension LightningService {

// MARK: - Probing

private static func scoringFeeParameters(config: Config) -> ScoringFeeParameters {
let defaultParameters = config.scoringFeeParams ?? defaultScoringFeeParameters

return ScoringFeeParameters(
basePenaltyMsat: scoringBasePenaltyMsat,
basePenaltyAmountMultiplierMsat: defaultParameters.basePenaltyAmountMultiplierMsat,
liquidityPenaltyMultiplierMsat: scoringLiquidityPenaltyMultiplierMsat,
liquidityPenaltyAmountMultiplierMsat: scoringLiquidityPenaltyAmountMultiplierMsat,
historicalLiquidityPenaltyMultiplierMsat: defaultParameters.historicalLiquidityPenaltyMultiplierMsat,
historicalLiquidityPenaltyAmountMultiplierMsat: scoringHistoricalLiquidityPenaltyAmountMultiplierMsat,
antiProbingPenaltyMsat: defaultParameters.antiProbingPenaltyMsat,
consideredImpossiblePenaltyMsat: scoringConsideredImpossiblePenaltyMsat,
linearSuccessProbability: defaultParameters.linearSuccessProbability,
probingDiversityPenaltyMsat: scoringProbingDiversityPenaltyMsat
)
}

/// Sends a probe to test if a payment route exists for the given invoice.
/// - Parameters:
/// - bolt11: The Lightning invoice string (BOLT 11)
Expand Down
4 changes: 2 additions & 2 deletions Bitkit/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -914,9 +914,9 @@ extension AppViewModel {
break
case .paymentForwarded:
break
case .probeSuccessful(paymentId: _, paymentHash: _):
case .probeSuccessful(paymentId: _, paymentHash: _, routeFeeMsat: _):
break
case .probeFailed(paymentId: _, paymentHash: _, shortChannelId: _):
case .probeFailed(paymentId: _, paymentHash: _, shortChannelId: _, routeFeeMsat: _):
break

// MARK: New Onchain Transaction Events
Expand Down
28 changes: 17 additions & 11 deletions Bitkit/ViewModels/WalletViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,19 +198,21 @@ class WalletViewModel: ObservableObject {

// Handle specific events for targeted UI updates
switch event {
case let .probeSuccessful(paymentId, paymentHash: paymentHash):
case let .probeSuccessful(paymentId, paymentHash: paymentHash, routeFeeMsat: routeFeeMsat):
self.cacheProbeOutcome(
success: true,
paymentId: paymentId,
paymentHash: paymentHash,
shortChannelId: nil
shortChannelId: nil,
routeFeeMsat: routeFeeMsat
)
case let .probeFailed(paymentId, paymentHash: paymentHash, shortChannelId: shortChannelId):
case let .probeFailed(paymentId, paymentHash: paymentHash, shortChannelId: shortChannelId, routeFeeMsat: routeFeeMsat):
self.cacheProbeOutcome(
success: false,
paymentId: paymentId,
paymentHash: paymentHash,
shortChannelId: shortChannelId
shortChannelId: shortChannelId.map(String.init),
routeFeeMsat: routeFeeMsat
)
case let .paymentReceived(_, paymentHash, _, _):
self.bolt11 = ""
Expand Down Expand Up @@ -692,7 +694,8 @@ class WalletViewModel: ObservableObject {
let success: Bool
let paymentId: PaymentId
let paymentHash: PaymentHash
let shortChannelId: UInt64?
let shortChannelId: String?
let routeFeeMsat: UInt64?
}

/// Waits for probe results that match one of the returned probe `paymentId`s.
Expand All @@ -717,23 +720,25 @@ class WalletViewModel: ObservableObject {
addOnEvent(id: eventId) { event in
guard !resumed else { return }
switch event {
case let .probeSuccessful(paymentId, paymentHash: paymentHash):
case let .probeSuccessful(paymentId, paymentHash: paymentHash, routeFeeMsat: routeFeeMsat):
guard pendingPaymentIds.contains(paymentId) else { return }
resumed = true
self.removeOnEvent(id: eventId)
continuation.resume(returning: .init(
success: true,
paymentId: paymentId,
paymentHash: paymentHash,
shortChannelId: nil
shortChannelId: nil,
routeFeeMsat: routeFeeMsat
))
case let .probeFailed(paymentId, paymentHash: paymentHash, shortChannelId: shortChannelId):
case let .probeFailed(paymentId, paymentHash: paymentHash, shortChannelId: shortChannelId, routeFeeMsat: routeFeeMsat):
guard pendingPaymentIds.remove(paymentId) != nil else { return }
lastFailure = .init(
success: false,
paymentId: paymentId,
paymentHash: paymentHash,
shortChannelId: shortChannelId
shortChannelId: shortChannelId.map(String.init),
routeFeeMsat: routeFeeMsat
)
if pendingPaymentIds.isEmpty, let lastFailure {
resumed = true
Expand All @@ -747,12 +752,13 @@ class WalletViewModel: ObservableObject {
}
}

private func cacheProbeOutcome(success: Bool, paymentId: PaymentId, paymentHash: PaymentHash, shortChannelId: UInt64?) {
private func cacheProbeOutcome(success: Bool, paymentId: PaymentId, paymentHash: PaymentHash, shortChannelId: String?, routeFeeMsat: UInt64?) {
probeOutcomes[paymentId] = ProbeOutcome(
success: success,
paymentId: paymentId,
paymentHash: paymentHash,
shortChannelId: shortChannelId
shortChannelId: shortChannelId,
routeFeeMsat: routeFeeMsat
)
}

Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Views/Settings/ProbingTool/ProbeResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import Foundation
struct ProbeResult {
let success: Bool
let durationMs: Int
let estimatedFeeSats: UInt64?
let routeFeeMsat: UInt64?
let errorMessage: String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ struct ProbeResultSectionView: View {
}
.font(.subheadline)

if let fee = result.estimatedFeeSats {
if let fee = result.routeFeeMsat {
HStack {
Image(systemName: "bitcoinsign.circle")
.foregroundStyle(.secondary)
Text("Estimated Fee")
Text("Route Fee")
Spacer()
Text("\(fee) sats")
Text("\(fee) msat")
.foregroundStyle(.secondary)
}
.font(.subheadline)
Expand Down
14 changes: 4 additions & 10 deletions Bitkit/Views/Settings/ProbingTool/ProbingToolScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,29 +249,23 @@ struct ProbingToolScreen: View {
let durationMs = Int(Date().timeIntervalSince(start) * 1000)

if resolved.success {
let estimatedFee: UInt64? = switch target {
case let .invoice(bolt11, _):
try? await lightningService.estimateRoutingFees(bolt11: bolt11, amountSats: amountSatsValue)
case .nodeId:
nil
}
await MainActor.run {
probeResult = ProbeResult(
success: true,
durationMs: durationMs,
estimatedFeeSats: estimatedFee,
routeFeeMsat: resolved.routeFeeMsat,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Optional Event Fee Hides Result

When an invoice probe succeeds but the event does not include routeFeeMsat, this path now stores nil and the fee row disappears. The old successful invoice path still called estimateRoutingFees, so users can lose the fee display even though the app can still compute an estimate for that invoice.

errorMessage: nil
)
}
app.toast(type: .success, title: "Probe Successful", description: "Route verified in \(durationMs) ms")
} else {
let scidText = resolved.shortChannelId.map(String.init) ?? "unknown"
let scidText = resolved.shortChannelId ?? "unknown"
let message = "Hash: \(resolved.paymentHash), SCID: \(scidText)"
await MainActor.run {
probeResult = ProbeResult(
success: false,
durationMs: durationMs,
estimatedFeeSats: nil,
routeFeeMsat: resolved.routeFeeMsat,
errorMessage: message
)
}
Expand All @@ -283,7 +277,7 @@ struct ProbingToolScreen: View {
probeResult = ProbeResult(
success: false,
durationMs: durationMs,
estimatedFeeSats: nil,
routeFeeMsat: nil,
errorMessage: error.localizedDescription
)
}
Expand Down
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.3.2] - 2026-07-14

### Fixed
- Improved Lightning probe route fee reporting. #622
- Improved Lightning payment route selection reliability. #623

## [2.3.1] - 2026-06-26

### Fixed
Expand Down Expand Up @@ -59,7 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix keyboard and UI issues in the calculator widget #513
- Preserve msat precision for LNURL pay, withdraw callbacks and bolt11 #512

[Unreleased]: https://github.com/synonymdev/bitkit-ios/compare/v2.3.1...HEAD
[Unreleased]: https://github.com/synonymdev/bitkit-ios/compare/v2.3.2...HEAD
[2.3.2]: https://github.com/synonymdev/bitkit-ios/compare/v2.3.1...v2.3.2
[2.3.1]: https://github.com/synonymdev/bitkit-ios/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/synonymdev/bitkit-ios/compare/v2.2.1...v2.3.0
[2.2.1]: https://github.com/synonymdev/bitkit-ios/compare/v2.2.0...v2.2.1
Expand Down
Loading