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
18 changes: 18 additions & 0 deletions Bitkit/Utilities/LightningAmountConversion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation

enum LightningAmountConversion {
/// Lightning amounts are commonly expressed in millisatoshis (msat).
///
/// The UI and amount input operate in whole sats. When converting a minimum bound to sats we must round up:
/// `100500 msat` means the minimum payable amount is `101 sat` (not `100 sat`).
static func satsCeil(fromMsats msats: UInt64) -> UInt64 {
let quotient = msats / Env.msatsPerSat
let remainder = msats % Env.msatsPerSat
return remainder == 0 ? quotient : quotient + 1
}

/// Converts msats → sats by rounding down (safe for maximum bounds).
static func satsFloor(fromMsats msats: UInt64) -> UInt64 {
msats / Env.msatsPerSat
}
}
8 changes: 4 additions & 4 deletions Bitkit/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ extension AppViewModel {
}

var normalizedData = data
normalizedData.minSendable = max(1, normalizedData.minSendable / Env.msatsPerSat)
normalizedData.maxSendable = max(normalizedData.minSendable, normalizedData.maxSendable / Env.msatsPerSat)
normalizedData.minSendable = max(1, LightningAmountConversion.satsCeil(fromMsats: normalizedData.minSendable))
normalizedData.maxSendable = max(normalizedData.minSendable, LightningAmountConversion.satsFloor(fromMsats: normalizedData.maxSendable))

// Check if user has enough lightning balance to pay the minimum amount
let lightningBalance = lightningService.balances?.totalLightningBalanceSats ?? 0
Expand Down Expand Up @@ -292,8 +292,8 @@ extension AppViewModel {
}

var normalizedData = data
let minSats = max(1, minMsats / Env.msatsPerSat)
let maxSats = max(minSats, maxMsats / Env.msatsPerSat)
let minSats = max(1, LightningAmountConversion.satsCeil(fromMsats: minMsats))
let maxSats = max(minSats, LightningAmountConversion.satsFloor(fromMsats: maxMsats))
normalizedData.minWithdrawable = minSats
normalizedData.maxWithdrawable = maxSats

Expand Down
8 changes: 5 additions & 3 deletions Bitkit/Views/Wallets/Sheets/BoostSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,8 @@ struct BoostSheet: View {
app.toast(
type: .success,
title: t("wallet__boost_success_title"),
description: t("wallet__boost_success_msg")
description: t("wallet__boost_success_msg"),
accessibilityIdentifier: "BoostSuccessToast"
)

Logger.info("Boost transaction completed successfully, hiding sheet", context: "BoostSheet.performBoost")
Expand All @@ -408,8 +409,9 @@ struct BoostSheet: View {

app.toast(
type: .error,
title: t("wallet__boost_error"),
description: error.localizedDescription
title: t("wallet__boost_error_title"),
description: t("wallet__boost_error_msg"),
accessibilityIdentifier: "BoostFailureToast"
)

throw error
Expand Down
20 changes: 20 additions & 0 deletions BitkitTests/LightningAmountConversionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@testable import Bitkit
import XCTest

final class LightningAmountConversionTests: XCTestCase {
func testSatsCeilRoundsUpWhenNotDivisibleBy1000() {
XCTAssertEqual(LightningAmountConversion.satsCeil(fromMsats: 100_500), 101)
XCTAssertEqual(LightningAmountConversion.satsCeil(fromMsats: 1500), 2)
}

func testSatsCeilKeepsExactSatAmounts() {
XCTAssertEqual(LightningAmountConversion.satsCeil(fromMsats: 100_000), 100)
XCTAssertEqual(LightningAmountConversion.satsCeil(fromMsats: 0), 0)
}

func testSatsFloorRoundsDown() {
XCTAssertEqual(LightningAmountConversion.satsFloor(fromMsats: 100_999), 100)
XCTAssertEqual(LightningAmountConversion.satsFloor(fromMsats: 100_000), 100)
XCTAssertEqual(LightningAmountConversion.satsFloor(fromMsats: 0), 0)
}
}
Loading