From 354e54daf21fff61421394e43057f9780f282f4b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 17:49:39 +0100 Subject: [PATCH 1/9] feat: channel balances move to spark at low fees --- Bitkit/MainNavView.swift | 6 + Bitkit/Managers/MoveToSparkManager.swift | 204 ++++++++++++++++++ .../TimedSheets/MoveToSparkTimedSheet.swift | 25 +++ .../TimedSheets/TimedSheetManager.swift | 2 + .../Localization/ar.lproj/Localizable.strings | 24 +++ .../Localization/ca.lproj/Localizable.strings | 24 +++ .../Localization/cs.lproj/Localizable.strings | 24 +++ .../Localization/de.lproj/Localizable.strings | 24 +++ .../Localization/el.lproj/Localizable.strings | 24 +++ .../Localization/en.lproj/Localizable.strings | 24 +++ .../es-419.lproj/Localizable.strings | 24 +++ .../Localization/es.lproj/Localizable.strings | 24 +++ .../Localization/fr.lproj/Localizable.strings | 24 +++ .../Localization/it.lproj/Localizable.strings | 24 +++ .../Localization/nl.lproj/Localizable.strings | 24 +++ .../Localization/pl.lproj/Localizable.strings | 24 +++ .../pt-BR.lproj/Localizable.strings | 24 +++ .../Localization/pt.lproj/Localizable.strings | 24 +++ .../Localization/ru.lproj/Localizable.strings | 24 +++ Bitkit/Services/LightningService.swift | 9 +- Bitkit/ViewModels/AppViewModel.swift | 8 +- Bitkit/ViewModels/NavigationViewModel.swift | 1 + Bitkit/ViewModels/SheetViewModel.swift | 13 ++ Bitkit/ViewModels/WalletViewModel.swift | 13 ++ Bitkit/Views/HomeScreen.swift | 10 + .../Advanced/LightningConnectionsView.swift | 11 + .../Settings/Advanced/MoveToSparkView.swift | 165 ++++++++++++++ Bitkit/Views/Sheets/MoveToSparkSheet.swift | 57 +++++ .../Views/Wallets/SpendingWalletScreen.swift | 12 ++ BitkitTests/MoveToSparkMathTests.swift | 33 +++ changelog.d/next/1206.added.md | 1 + journeys/transfer/move-to-spark.xml | 29 +++ 32 files changed, 955 insertions(+), 4 deletions(-) create mode 100644 Bitkit/Managers/MoveToSparkManager.swift create mode 100644 Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift create mode 100644 Bitkit/Views/Settings/Advanced/MoveToSparkView.swift create mode 100644 Bitkit/Views/Sheets/MoveToSparkSheet.swift create mode 100644 BitkitTests/MoveToSparkMathTests.swift create mode 100644 changelog.d/next/1206.added.md create mode 100644 journeys/transfer/move-to-spark.xml diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index 0b7cf1540..3c20c384b 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -232,6 +232,11 @@ struct MainNavView: View { ) { config in SecuritySheet(config: config) } + .sheet(item: $sheets.moveToSparkSheetItem, onDismiss: { + sheets.hideSheetIfActive(.moveToSpark, reason: "Move to Spark intro dismissed") + }) { config in + MoveToSparkSheet(config: config) + } .sheet(item: $sheets.usdtIntroSheetItem, onDismiss: { sheets.hideSheetIfActive(.usdtIntro, reason: "USDT intro dismissed") }) { config in @@ -705,6 +710,7 @@ struct MainNavView: View { case .connections: LightningConnectionsView() case let .connectionDetail(channelId): LightningConnectionDetailView(channelId: channelId) case .sparkConnectionDetail: SparkConnectionDetailView() + case .moveToSpark: MoveToSparkView() case let .closeConnection(channel: channel): CloseConnectionConfirmation(channel: channel) case .node: NodeStateView() case .electrumSettings: ElectrumSettingsScreen() diff --git a/Bitkit/Managers/MoveToSparkManager.swift b/Bitkit/Managers/MoveToSparkManager.swift new file mode 100644 index 000000000..980d6ee42 --- /dev/null +++ b/Bitkit/Managers/MoveToSparkManager.swift @@ -0,0 +1,204 @@ +import Foundation +import LDKNode + +/// The amounts behind moving the channels' balance to Spark, kept apart so they can be tested without a node. +enum MoveToSparkMath { + /// Below this a move is not offered: what is left is about the routing fee. + static let minMoveSats: UInt64 = 1000 + /// The fee guessed for the first quote of a full move, as a share of the amount in parts per million. + private static let firstFeeGuessPpm: UInt64 = 5000 + /// The least fee guessed for the first quote of a full move. + private static let minFeeGuessSats: UInt64 = 10 + /// Room left above the quoted fee, in sats, for the fee moving between the quote and the payment. + private static let feeMarginSats: UInt64 = 2 + + static func movableSats(channels: [ChannelDetails]?) -> UInt64 { + (channels ?? []).filter(\.isUsable).map { $0.nextOutboundHtlcLimitMsat / 1000 }.reduce(0, +) + } + + /// Whether the channels hold a balance worth moving to a usable Spark wallet. + static func canMove(channels: [ChannelDetails]?, isSparkUsable: Bool) -> Bool { + isSparkUsable && movableSats(channels: channels) >= minMoveSats + } + + /// The first amount to quote: `wantedSats` when it leaves room for fees, else the limit less a guessed fee. + static func firstCandidate(wantedSats: UInt64, limitSats: UInt64) -> UInt64 { + let guessSats = max(limitSats.multipliedReportingOverflow(by: firstFeeGuessPpm).partialValue / 1_000_000, minFeeGuessSats) + return min(wantedSats, amountAfterFee(limitSats: limitSats, feeSats: guessSats)) + } + + /// The amount that fits under `limitSats` together with `feeSats` and its margin. + static func amountAfterFee(limitSats: UInt64, feeSats: UInt64) -> UInt64 { + let reserved = feeSats.addingReportingOverflow(feeMarginSats) + return reserved.overflow || reserved.partialValue >= limitSats ? 0 : limitSats - reserved.partialValue + } + + /// The routing fee cap for the payment: the quoted fee and its margin, so it never pays more than shown. + static func feeCapMsat(feeSats: UInt64) -> UInt64 { + (feeSats + feeMarginSats) * 1000 + } + + /// The smaller amount to offer after one did not route: half of it, or nothing below the minimum. + static func smallerAmount(_ sats: UInt64) -> UInt64? { + let half = sats / 2 + return half >= minMoveSats ? half : nil + } +} + +struct MoveToSparkQuote: Equatable { + let invoice: String + let amountSats: UInt64 + let feeSats: UInt64 +} + +enum MoveToSparkError: Error { + case nothingToMove + case sparkUnavailable + /// `amountSats` did not route from the channels to Spark in one payment. + case tooLarge(amountSats: UInt64) +} + +/// The payments that move a channel balance to Spark, whose send and receive the app does not announce as payments. +@MainActor +enum MoveToSparkPayments { + private static var paymentHashes = Set() + + static func insert(_ paymentHash: String) { + paymentHashes.insert(paymentHash) + } + + static func contains(_ paymentHash: String?) -> Bool { + paymentHash.map(paymentHashes.contains) ?? false + } +} + +enum MoveToSparkFailure { + case nothingToMove + case sparkUnavailable + case unknown +} + +enum MoveToSparkState: Equatable { + case quoting + case confirm(MoveToSparkQuote, isMoving: Bool) + /// `hasRest`: the channels can still send enough to move again, as after a smaller amount. + case done(amountSats: UInt64, feeSats: UInt64, closeChannel: ChannelDetails?, hasRest: Bool) + /// `amountSats` did not route in one payment; `smallerSats` is the next amount to try, if any is left. + case tooLarge(amountSats: UInt64, smallerSats: UInt64?) + case failed(MoveToSparkFailure) + + static func == (lhs: MoveToSparkState, rhs: MoveToSparkState) -> Bool { + switch (lhs, rhs) { + case (.quoting, .quoting): true + case let (.confirm(a, am), .confirm(b, bm)): a == b && am == bm + case let (.done(a, af, ac, ar), .done(b, bf, bc, br)): a == b && af == bf && ac?.channelId == bc?.channelId && ar == br + case let (.tooLarge(a, asm), .tooLarge(b, bsm)): a == b && asm == bsm + case let (.failed(a), .failed(b)): a == b + default: false + } + } +} + +/// Moves the channels' balance to the user's own Spark wallet, paying a Spark invoice over the channels only. +/// Blocktank is closing its channels, and this is the cheapest way to keep the balance in Spending. +@Observable @MainActor +final class MoveToSparkManager { + private static let invoiceDescription = "Move to Spark" + private static let invoiceExpirySecs: UInt32 = 3600 + private static let maxQuoteRounds = 4 + + private(set) var state: MoveToSparkState = .quoting + + private let lightningService: LightningService + private let spark: SparkManager + + init() { + lightningService = .shared + spark = .shared + } + + /// Quotes moving `targetSats`, or everything the channels can send. + func quote(wallet: WalletViewModel, targetSats: UInt64? = nil) async { + state = .quoting + do { + state = try await .confirm(makeQuote(channels: wallet.channels, targetSats: targetSats), isMoving: false) + } catch { + Logger.warn("Failed to quote move to Spark of '\(String(describing: targetSats))': \(error)", context: "MoveToSpark") + state = Self.state(for: error) + } + } + + func move(wallet: WalletViewModel) async { + guard case let .confirm(quote, isMoving) = state, !isMoving else { return } + state = .confirm(quote, isMoving: true) + do { + let paymentHash = try Bolt11Invoice.fromStr(invoiceStr: quote.invoice).paymentHash() + MoveToSparkPayments.insert(String(describing: paymentHash)) + let settled = try await wallet.sendOverChannels( + bolt11: quote.invoice, + sats: quote.amountSats, + maxFeeMsat: MoveToSparkMath.feeCapMsat(feeSats: quote.feeSats) + ) + wallet.syncState() + let openChannels = (wallet.channels ?? []).filter(\.isChannelReady) + state = .done( + amountSats: quote.amountSats, + feeSats: settled.feePaidSats, + closeChannel: openChannels.count == 1 ? openChannels.first : nil, + hasRest: MoveToSparkMath.movableSats(channels: wallet.channels) >= MoveToSparkMath.minMoveSats + ) + } catch { + Logger.error("Failed to move '\(quote.amountSats)' sats to Spark: \(error)", context: "MoveToSpark") + state = .tooLarge(amountSats: quote.amountSats, smallerSats: MoveToSparkMath.smallerAmount(quote.amountSats)) + } + } + + func makeQuote(channels: [ChannelDetails]?, targetSats: UInt64?) async throws -> MoveToSparkQuote { + let limitSats = MoveToSparkMath.movableSats(channels: channels) + let wantedSats = min(targetSats ?? limitSats, limitSats) + guard wantedSats >= MoveToSparkMath.minMoveSats else { throw MoveToSparkError.nothingToMove } + guard spark.isReady, !spark.isExiting else { throw MoveToSparkError.sparkUnavailable } + + let invoice = try await spark.receive( + amountSats: nil, + description: Self.invoiceDescription, + expirySecs: Self.invoiceExpirySecs + ) + var amountSats = MoveToSparkMath.firstCandidate(wantedSats: wantedSats, limitSats: limitSats) + var fitting: MoveToSparkQuote? + // The first amount leaves room for a guessed fee; once the real fee is known, the amount grows to fit it. + for _ in 0 ..< Self.maxQuoteRounds where amountSats >= MoveToSparkMath.minMoveSats { + let feeSats: UInt64 + do { + feeSats = try await lightningService.estimateRoutingFees(bolt11: invoice, amountSats: amountSats, channelsOnly: true) + } catch { + if let fitting { + return fitting + } + throw MoveToSparkError.tooLarge(amountSats: amountSats) + } + let nextSats = min(wantedSats, MoveToSparkMath.amountAfterFee(limitSats: limitSats, feeSats: feeSats)) + if amountSats + feeSats <= limitSats { + let quote = MoveToSparkQuote(invoice: invoice, amountSats: amountSats, feeSats: feeSats) + fitting = quote + if nextSats <= amountSats { + return quote + } + } + amountSats = nextSats + } + if let fitting { + return fitting + } + throw MoveToSparkError.tooLarge(amountSats: amountSats) + } + + private static func state(for error: Error) -> MoveToSparkState { + switch error as? MoveToSparkError { + case let .tooLarge(amountSats): .tooLarge(amountSats: amountSats, smallerSats: MoveToSparkMath.smallerAmount(amountSats)) + case .nothingToMove: .failed(.nothingToMove) + case .sparkUnavailable: .failed(.sparkUnavailable) + case nil: .failed(.unknown) + } + } +} diff --git a/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift b/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift new file mode 100644 index 000000000..826077406 --- /dev/null +++ b/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift @@ -0,0 +1,25 @@ +import Foundation + +/// Shown once after the update to users whose channels still hold a balance worth moving to Spark. +struct MoveToSparkTimedSheet: TimedSheetItem { + let id = SheetID.moveToSpark + let priority = TimedSheetPriority.critical + let sheetItem: any SheetItem = MoveToSparkSheetItem() + let app: AppViewModel + let wallet: WalletViewModel + + @MainActor + func shouldShow() async -> Bool { + guard !app.hasSeenMoveToSparkIntro else { return false } + let spark = SparkManager.shared + return MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: spark.isReady && !spark.isExiting) + } + + func onShown() { + Task { @MainActor in + app.hasSeenMoveToSparkIntro = true + } + } + + func onDismissed() {} +} diff --git a/Bitkit/Managers/TimedSheets/TimedSheetManager.swift b/Bitkit/Managers/TimedSheets/TimedSheetManager.swift index f3ba85bdf..bc9cf01de 100644 --- a/Bitkit/Managers/TimedSheets/TimedSheetManager.swift +++ b/Bitkit/Managers/TimedSheets/TimedSheetManager.swift @@ -6,6 +6,7 @@ enum TimedSheetPriority: Int, CaseIterable { case low = 1 case medium = 2 case high = 3 + case critical = 4 } /// Protocol for timed sheet items @@ -75,6 +76,7 @@ class TimedSheetManager: ObservableObject { registerSheet(notificationsSheet) registerSheet(UsdtIntroTimedSheet(wallet: walletViewModel, settings: settingsViewModel)) + registerSheet(MoveToSparkTimedSheet(app: appViewModel, wallet: walletViewModel)) let quickpaySheet = QuickpayTimedSheet(appViewModel: appViewModel, walletViewModel: walletViewModel) registerSheet(quickpaySheet) diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index 2ba6f3041..8292254a8 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -292,6 +292,30 @@ "lightning__transaction" = "المعاملة"; "lightning__order_fee" = "رسوم الطلب"; "lightning__balance" = "الرصيد"; +"lightning__move_to_spark__amount" = "المبلغ"; +"lightning__move_to_spark__close" = "إغلاق الاتصال"; +"lightning__move_to_spark__confirm_text" = "ينتقل رصيدك إلى Spark عبر Lightning، وهي أقل طريقة رسومًا للإبقاء عليه في الإنفاق. رسوم التوجيه أدناه هي أقصى ما تدفعه."; +"lightning__move_to_spark__confirm_title" = "الانتقال إلى Spark"; +"lightning__move_to_spark__done_rest_text" = "جزء من رصيدك الآن على Spark. انقل الباقي بالطريقة نفسها، ثم أغلق الاتصال الذي أصبح فارغًا."; +"lightning__move_to_spark__done_text" = "رصيدك الآن على Spark. أغلق الاتصال الذي أصبح فارغًا لإعادة احتياطيه إلى المدخرات."; +"lightning__move_to_spark__done_title" = "تم النقل إلى Spark"; +"lightning__move_to_spark__failed_nothing" = "لم يتبق في اتصالات Lightning لديك رصيد للنقل."; +"lightning__move_to_spark__failed_spark" = "Spark غير جاهز للاستلام بعد. حاول مرة أخرى بعد قليل."; +"lightning__move_to_spark__failed_title" = "تعذّر النقل"; +"lightning__move_to_spark__failed_unknown" = "حدث خطأ أثناء تحضير النقل. حاول مرة أخرى."; +"lightning__move_to_spark__fee" = "رسوم التوجيه"; +"lightning__move_to_spark__in_flight_text" = "الدفعة في طريقها وستظهر في Spark بمجرد إتمامها. ثم أغلق الاتصال الفارغ لإعادة احتياطيه إلى المدخرات."; +"lightning__move_to_spark__intro_text" = "يقوم Blocktank بإغلاق اتصالات Lightning الخاصة به. انقل رصيدك إلى Spark الآن: فهو الخيار الأقل رسومًا، ويبقى ضمن الإنفاق."; +"lightning__move_to_spark__intro_title" = "اتصالك قيد الإغلاق"; +"lightning__move_to_spark__move_rest" = "نقل الباقي"; +"lightning__move_to_spark__nav_title" = "النقل إلى Spark"; +"lightning__move_to_spark__quoting" = "جارٍ البحث عن مسار ورسومه…"; +"lightning__move_to_spark__swipe" = "اسحب للنقل"; +"lightning__move_to_spark__too_large_none" = "أغلق الاتصال بدلاً من ذلك لإعادة الرصيد إلى المدخرات."; +"lightning__move_to_spark__too_large_smaller" = "يمكنك نقله على دفعات أصغر."; +"lightning__move_to_spark__too_large_text" = "{amount} أكبر من أن يُوجَّه إلى Spark في دفعة واحدة."; +"lightning__move_to_spark__too_large_title" = "المبلغ كبير جدًا"; +"lightning__move_to_spark__try" = "نقل {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "رسوم المطالبة"; "lightning__spark_confirmations" = "{count}/3 تأكيدات"; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index 78a612fe0..a3c35f2e6 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -329,6 +329,30 @@ "lightning__transaction" = "Transacció"; "lightning__order_fee" = "Comissió de comanda"; "lightning__balance" = "Saldo"; +"lightning__move_to_spark__amount" = "Import"; +"lightning__move_to_spark__close" = "Tanca la connexió"; +"lightning__move_to_spark__confirm_text" = "El teu saldo passa a Spark per Lightning, la manera amb menys tarifes de mantenir-lo a Despesa. La tarifa d'enrutament de sota és el màxim que pagues."; +"lightning__move_to_spark__confirm_title" = "Moure a Spark"; +"lightning__move_to_spark__done_rest_text" = "Una part del teu saldo ja és a Spark. Mou la resta de la mateixa manera i després tanca la connexió buida."; +"lightning__move_to_spark__done_text" = "El teu saldo ja és a Spark. Tanca la connexió buida per tornar-ne la reserva als Estalvis."; +"lightning__move_to_spark__done_title" = "Mogut a Spark"; +"lightning__move_to_spark__failed_nothing" = "Les teves connexions Lightning ja no tenen saldo per moure."; +"lightning__move_to_spark__failed_spark" = "Spark encara no està a punt per rebre. Torna-ho a provar d'aquí a un moment."; +"lightning__move_to_spark__failed_title" = "No s'ha pogut moure"; +"lightning__move_to_spark__failed_unknown" = "Alguna cosa ha anat malament en preparar el moviment. Torna-ho a provar."; +"lightning__move_to_spark__fee" = "Tarifa d'enrutament"; +"lightning__move_to_spark__in_flight_text" = "El pagament està en camí i apareixerà a Spark quan s'hagi liquidat. Després tanca la connexió buida per tornar-ne la reserva als Estalvis."; +"lightning__move_to_spark__intro_text" = "Blocktank està tancant les seves connexions Lightning. Mou ara el teu saldo a Spark: és l'opció amb menys tarifes i es manté a Despesa."; +"lightning__move_to_spark__intro_title" = "La teva connexió es tanca"; +"lightning__move_to_spark__move_rest" = "Moure la resta"; +"lightning__move_to_spark__nav_title" = "Moure a Spark"; +"lightning__move_to_spark__quoting" = "Cercant una ruta i la seva tarifa…"; +"lightning__move_to_spark__swipe" = "Desplaça per moure"; +"lightning__move_to_spark__too_large_none" = "Tanca la connexió per tornar el saldo als Estalvis."; +"lightning__move_to_spark__too_large_smaller" = "El pots moure en parts més petites."; +"lightning__move_to_spark__too_large_text" = "{amount} és massa gran per enrutar-se a Spark en un sol pagament."; +"lightning__move_to_spark__too_large_title" = "Import massa gran"; +"lightning__move_to_spark__try" = "Moure {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Comissió de reclamació"; "lightning__spark_confirmations" = "{count}/3 confirmacions"; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index 755de70ef..09fb959fc 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -345,6 +345,30 @@ "lightning__transaction" = "Transakce"; "lightning__order_fee" = "Poplatek za objednávku"; "lightning__balance" = "Zůstatek"; +"lightning__move_to_spark__amount" = "Částka"; +"lightning__move_to_spark__close" = "Uzavřít spojení"; +"lightning__move_to_spark__confirm_text" = "Váš zůstatek se přesune do Spark přes Lightning, což je způsob s nejnižšími poplatky, jak jej udržet v Útratách. Poplatek za směrování níže je maximum, které zaplatíte."; +"lightning__move_to_spark__confirm_title" = "Přesun do Spark"; +"lightning__move_to_spark__done_rest_text" = "Část vašeho zůstatku je nyní ve Spark. Zbytek přesuňte stejným způsobem a poté uzavřete vyprázdněné spojení."; +"lightning__move_to_spark__done_text" = "Váš zůstatek je nyní ve Spark. Uzavřete vyprázdněné spojení, aby se jeho rezerva vrátila do Úspor."; +"lightning__move_to_spark__done_title" = "Přesunuto do Spark"; +"lightning__move_to_spark__failed_nothing" = "Vaše spojení Lightning už nemají žádný zůstatek k přesunu."; +"lightning__move_to_spark__failed_spark" = "Spark zatím není připraven přijímat. Zkuste to za chvíli znovu."; +"lightning__move_to_spark__failed_title" = "Přesun se nezdařil"; +"lightning__move_to_spark__failed_unknown" = "Při přípravě přesunu se něco pokazilo. Zkuste to znovu."; +"lightning__move_to_spark__fee" = "Poplatek za směrování"; +"lightning__move_to_spark__in_flight_text" = "Platba je na cestě a ve Spark se zobrazí po vypořádání. Poté uzavřete vyprázdněné spojení, aby se jeho rezerva vrátila do Úspor."; +"lightning__move_to_spark__intro_text" = "Blocktank uzavírá svá spojení Lightning. Přesuňte svůj zůstatek do Spark hned: je to možnost s nejnižšími poplatky a zůstane v Útratách."; +"lightning__move_to_spark__intro_title" = "Vaše spojení se uzavírá"; +"lightning__move_to_spark__move_rest" = "Přesunout zbytek"; +"lightning__move_to_spark__nav_title" = "Přesun do Spark"; +"lightning__move_to_spark__quoting" = "Hledání trasy a jejího poplatku…"; +"lightning__move_to_spark__swipe" = "Přejetím přesuňte"; +"lightning__move_to_spark__too_large_none" = "Místo toho uzavřete spojení, aby se zůstatek vrátil do Úspor."; +"lightning__move_to_spark__too_large_smaller" = "Můžete jej přesunout po menších částech."; +"lightning__move_to_spark__too_large_text" = "{amount} je příliš velká částka na směrování do Spark v jedné platbě."; +"lightning__move_to_spark__too_large_title" = "Částka je příliš velká"; +"lightning__move_to_spark__try" = "Přesunout {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Poplatek za připsání"; "lightning__spark_confirmations" = "{count}/3 potvrzení"; diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings index 41d543636..e6159b281 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -343,6 +343,30 @@ "lightning__transaction" = "Transaktion"; "lightning__order_fee" = "Auftragsgebühr"; "lightning__balance" = "Guthaben"; +"lightning__move_to_spark__amount" = "Betrag"; +"lightning__move_to_spark__close" = "Verbindung schließen"; +"lightning__move_to_spark__confirm_text" = "Ihr Guthaben wechselt über Lightning zu Spark, dem günstigsten Weg, es im Ausgabenkonto zu behalten. Die Routing-Gebühr unten ist das Maximum, das Sie zahlen."; +"lightning__move_to_spark__confirm_title" = "Zu Spark verschieben"; +"lightning__move_to_spark__done_rest_text" = "Ein Teil Ihres Guthabens ist jetzt bei Spark. Verschieben Sie den Rest auf dieselbe Weise und schließen Sie dann die geleerte Verbindung."; +"lightning__move_to_spark__done_text" = "Ihr Guthaben ist jetzt bei Spark. Schließen Sie die geleerte Verbindung, um deren Reserve an das Sparkonto zurückzugeben."; +"lightning__move_to_spark__done_title" = "Zu Spark verschoben"; +"lightning__move_to_spark__failed_nothing" = "Ihre Lightning-Verbindungen haben kein Guthaben mehr zum Verschieben."; +"lightning__move_to_spark__failed_spark" = "Spark ist noch nicht bereit zum Empfangen. Versuchen Sie es gleich noch einmal."; +"lightning__move_to_spark__failed_title" = "Verschieben fehlgeschlagen"; +"lightning__move_to_spark__failed_unknown" = "Beim Vorbereiten der Verschiebung ist etwas schiefgelaufen. Versuchen Sie es erneut."; +"lightning__move_to_spark__fee" = "Routing-Gebühr"; +"lightning__move_to_spark__in_flight_text" = "Die Zahlung ist unterwegs und erscheint in Spark, sobald sie abgeschlossen ist. Schließen Sie dann die geleerte Verbindung, um deren Reserve an das Sparkonto zurückzugeben."; +"lightning__move_to_spark__intro_text" = "Blocktank schließt seine Lightning-Verbindungen. Verschieben Sie Ihr Guthaben jetzt zu Spark: Das ist die günstigste Option, und es bleibt im Ausgabenkonto."; +"lightning__move_to_spark__intro_title" = "Ihre Verbindung wird geschlossen"; +"lightning__move_to_spark__move_rest" = "Rest verschieben"; +"lightning__move_to_spark__nav_title" = "Zu Spark verschieben"; +"lightning__move_to_spark__quoting" = "Route und Gebühr werden gesucht…"; +"lightning__move_to_spark__swipe" = "Swipe zum Verschieben"; +"lightning__move_to_spark__too_large_none" = "Schließen Sie stattdessen die Verbindung, um das Guthaben an das Sparkonto zurückzugeben."; +"lightning__move_to_spark__too_large_smaller" = "Sie können es in kleineren Teilen verschieben."; +"lightning__move_to_spark__too_large_text" = "{amount} ist zu groß, um in einer Zahlung zu Spark geroutet zu werden."; +"lightning__move_to_spark__too_large_title" = "Betrag zu groß"; +"lightning__move_to_spark__try" = "{amount} verschieben"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Einlösegebühr"; "lightning__spark_confirmations" = "{count}/3 Bestätigungen"; diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings index 3bcdf21e1..282d46583 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -316,6 +316,30 @@ "lightning__transaction" = "Συναλλαγή"; "lightning__order_fee" = "Τέλος παραγγελίας"; "lightning__balance" = "Υπόλοιπο"; +"lightning__move_to_spark__amount" = "Ποσό"; +"lightning__move_to_spark__close" = "Κλείσιμο σύνδεσης"; +"lightning__move_to_spark__confirm_text" = "Το υπόλοιπό σας μεταφέρεται στο Spark μέσω Lightning, τον τρόπο με τα χαμηλότερα τέλη για να παραμείνει στις Δαπάνες. Το τέλος δρομολόγησης παρακάτω είναι το μέγιστο που πληρώνετε."; +"lightning__move_to_spark__confirm_title" = "Μεταφορά στο Spark"; +"lightning__move_to_spark__done_rest_text" = "Ένα μέρος του υπολοίπου σας βρίσκεται πλέον στο Spark. Μεταφέρετε τα υπόλοιπα με τον ίδιο τρόπο και έπειτα κλείστε την άδεια σύνδεση."; +"lightning__move_to_spark__done_text" = "Το υπόλοιπό σας βρίσκεται πλέον στο Spark. Κλείστε την άδεια σύνδεση για να επιστρέψει το απόθεμά της στις Αποταμιεύσεις."; +"lightning__move_to_spark__done_title" = "Μεταφέρθηκε στο Spark"; +"lightning__move_to_spark__failed_nothing" = "Οι συνδέσεις Lightning σας δεν έχουν πλέον υπόλοιπο για μεταφορά."; +"lightning__move_to_spark__failed_spark" = "Το Spark δεν είναι ακόμη έτοιμο να λάβει. Δοκιμάστε ξανά σε λίγο."; +"lightning__move_to_spark__failed_title" = "Αποτυχία μεταφοράς"; +"lightning__move_to_spark__failed_unknown" = "Κάτι πήγε στραβά κατά την προετοιμασία της μεταφοράς. Δοκιμάστε ξανά."; +"lightning__move_to_spark__fee" = "Τέλος δρομολόγησης"; +"lightning__move_to_spark__in_flight_text" = "Η πληρωμή βρίσκεται σε εξέλιξη και θα εμφανιστεί στο Spark μόλις διακανονιστεί. Έπειτα κλείστε την άδεια σύνδεση για να επιστρέψει το απόθεμά της στις Αποταμιεύσεις."; +"lightning__move_to_spark__intro_text" = "Το Blocktank κλείνει τις συνδέσεις Lightning του. Μεταφέρετε το υπόλοιπό σας στο Spark τώρα: είναι η επιλογή με τα χαμηλότερα τέλη και παραμένει στις Δαπάνες."; +"lightning__move_to_spark__intro_title" = "Η σύνδεσή σας κλείνει"; +"lightning__move_to_spark__move_rest" = "Μεταφορά των υπολοίπων"; +"lightning__move_to_spark__nav_title" = "Μεταφορά στο Spark"; +"lightning__move_to_spark__quoting" = "Εύρεση διαδρομής και του τέλους της…"; +"lightning__move_to_spark__swipe" = "Σύρε για μεταφορά"; +"lightning__move_to_spark__too_large_none" = "Κλείστε αντί αυτού τη σύνδεση για να επιστρέψει το υπόλοιπο στις Αποταμιεύσεις."; +"lightning__move_to_spark__too_large_smaller" = "Μπορείτε να το μεταφέρετε σε μικρότερα μέρη."; +"lightning__move_to_spark__too_large_text" = "Το {amount} είναι πολύ μεγάλο για δρομολόγηση στο Spark με μία πληρωμή."; +"lightning__move_to_spark__too_large_title" = "Το ποσό είναι πολύ μεγάλο"; +"lightning__move_to_spark__try" = "Μεταφορά {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Χρέωση εξαργύρωσης"; "lightning__spark_confirmations" = "{count}/3 επιβεβαιώσεις"; diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 9f299e68b..fb59f88ab 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -338,6 +338,30 @@ "lightning__transaction" = "Transaction"; "lightning__order_fee" = "Order fee"; "lightning__balance" = "Balance"; +"lightning__move_to_spark__amount" = "Amount"; +"lightning__move_to_spark__close" = "Close Connection"; +"lightning__move_to_spark__confirm_text" = "Your balance moves to Spark over Lightning, the lowest-fee way to keep it in Spending. The routing fee below is the most you pay."; +"lightning__move_to_spark__confirm_title" = "Move to Spark"; +"lightning__move_to_spark__done_rest_text" = "Part of your balance is on Spark now. Move the rest the same way, then close the emptied connection."; +"lightning__move_to_spark__done_text" = "Your balance is on Spark now. Close the emptied connection to return its reserve to Savings."; +"lightning__move_to_spark__done_title" = "Moved to Spark"; +"lightning__move_to_spark__failed_nothing" = "Your Lightning connections have no balance left to move."; +"lightning__move_to_spark__failed_spark" = "Spark is not ready to receive yet. Try again in a moment."; +"lightning__move_to_spark__failed_title" = "Could not move"; +"lightning__move_to_spark__failed_unknown" = "Something went wrong while preparing the move. Try again."; +"lightning__move_to_spark__fee" = "Routing fee"; +"lightning__move_to_spark__in_flight_text" = "The payment is on its way and shows in Spark once it settles. Then close the emptied connection to return its reserve to Savings."; +"lightning__move_to_spark__intro_text" = "Blocktank is closing its Lightning connections. Move your balance to Spark now: it is the lowest-fee option, and it stays in Spending."; +"lightning__move_to_spark__intro_title" = "Your connection is closing"; +"lightning__move_to_spark__move_rest" = "Move The Rest"; +"lightning__move_to_spark__nav_title" = "Move To Spark"; +"lightning__move_to_spark__quoting" = "Finding a route and its fee…"; +"lightning__move_to_spark__swipe" = "Swipe To Move"; +"lightning__move_to_spark__too_large_none" = "Close the connection instead to return the balance to Savings."; +"lightning__move_to_spark__too_large_smaller" = "You can move it in smaller parts."; +"lightning__move_to_spark__too_large_text" = "{amount} is too large to route to Spark in one payment."; +"lightning__move_to_spark__too_large_title" = "Amount too large"; +"lightning__move_to_spark__try" = "Move {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Claim fee"; "lightning__spark_confirmations" = "{count}/3 confirmations"; diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings index 7de3e658a..bc525c9af 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -346,6 +346,30 @@ "lightning__transaction" = "Transacción"; "lightning__order_fee" = "Tarifa de pedido"; "lightning__balance" = "Saldo"; +"lightning__move_to_spark__amount" = "Monto"; +"lightning__move_to_spark__close" = "Terminar conexión"; +"lightning__move_to_spark__confirm_text" = "Su saldo pasa a Spark por Lightning, la forma de menor comisión para mantenerlo en Gasto. La comisión de enrutamiento de abajo es lo máximo que paga."; +"lightning__move_to_spark__confirm_title" = "Mover a Spark"; +"lightning__move_to_spark__done_rest_text" = "Una parte de su saldo ya está en Spark. Mueva el resto de la misma forma y luego termine la conexión vacía."; +"lightning__move_to_spark__done_text" = "Su saldo ya está en Spark. Termine la conexión vacía para devolver su reserva a Ahorro."; +"lightning__move_to_spark__done_title" = "Movido a Spark"; +"lightning__move_to_spark__failed_nothing" = "Sus conexiones Lightning ya no tienen saldo para mover."; +"lightning__move_to_spark__failed_spark" = "Spark aún no está listo para recibir. Inténtelo de nuevo en un momento."; +"lightning__move_to_spark__failed_title" = "No se pudo mover"; +"lightning__move_to_spark__failed_unknown" = "Algo salió mal al preparar el movimiento. Inténtelo de nuevo."; +"lightning__move_to_spark__fee" = "Comisión de enrutamiento"; +"lightning__move_to_spark__in_flight_text" = "El pago está en camino y aparecerá en Spark cuando se complete. Luego termine la conexión vacía para devolver su reserva a Ahorro."; +"lightning__move_to_spark__intro_text" = "Blocktank está cerrando sus conexiones Lightning. Mueva su saldo a Spark ahora: es la opción de menor comisión y se mantiene en Gasto."; +"lightning__move_to_spark__intro_title" = "Su conexión se está cerrando"; +"lightning__move_to_spark__move_rest" = "Mover el resto"; +"lightning__move_to_spark__nav_title" = "Mover a Spark"; +"lightning__move_to_spark__quoting" = "Buscando una ruta y su comisión…"; +"lightning__move_to_spark__swipe" = "Deslizar para mover"; +"lightning__move_to_spark__too_large_none" = "Termine la conexión para devolver el saldo a Ahorro."; +"lightning__move_to_spark__too_large_smaller" = "Puede moverlo en partes más pequeñas."; +"lightning__move_to_spark__too_large_text" = "{amount} es demasiado grande para enrutarse a Spark en un solo pago."; +"lightning__move_to_spark__too_large_title" = "Monto demasiado grande"; +"lightning__move_to_spark__try" = "Mover {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Comisión de reclamo"; "lightning__spark_confirmations" = "{count}/3 confirmaciones"; diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings index 510f68918..ce6cd6080 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -330,6 +330,30 @@ "lightning__transaction" = "Transacción"; "lightning__order_fee" = "Tarifa de pedido"; "lightning__balance" = "Saldo"; +"lightning__move_to_spark__amount" = "Cantidad"; +"lightning__move_to_spark__close" = "Terminar conexión"; +"lightning__move_to_spark__confirm_text" = "Su saldo pasa a Spark por Lightning, la forma de menor comisión para mantenerlo en Gasto. La comisión de enrutamiento de abajo es lo máximo que paga."; +"lightning__move_to_spark__confirm_title" = "Mover a Spark"; +"lightning__move_to_spark__done_rest_text" = "Una parte de su saldo ya está en Spark. Mueva el resto de la misma forma y luego termine la conexión vacía."; +"lightning__move_to_spark__done_text" = "Su saldo ya está en Spark. Termine la conexión vacía para devolver su reserva a Ahorro."; +"lightning__move_to_spark__done_title" = "Movido a Spark"; +"lightning__move_to_spark__failed_nothing" = "Sus conexiones Lightning ya no tienen saldo para mover."; +"lightning__move_to_spark__failed_spark" = "Spark aún no está listo para recibir. Inténtelo de nuevo en un momento."; +"lightning__move_to_spark__failed_title" = "No se pudo mover"; +"lightning__move_to_spark__failed_unknown" = "Algo salió mal al preparar el movimiento. Inténtelo de nuevo."; +"lightning__move_to_spark__fee" = "Comisión de enrutamiento"; +"lightning__move_to_spark__in_flight_text" = "El pago está en camino y aparecerá en Spark cuando se complete. Luego termine la conexión vacía para devolver su reserva a Ahorro."; +"lightning__move_to_spark__intro_text" = "Blocktank está cerrando sus conexiones Lightning. Mueva su saldo a Spark ahora: es la opción de menor comisión y se mantiene en Gasto."; +"lightning__move_to_spark__intro_title" = "Su conexión se está cerrando"; +"lightning__move_to_spark__move_rest" = "Mover el resto"; +"lightning__move_to_spark__nav_title" = "Mover a Spark"; +"lightning__move_to_spark__quoting" = "Buscando una ruta y su comisión…"; +"lightning__move_to_spark__swipe" = "Deslizar para mover"; +"lightning__move_to_spark__too_large_none" = "Termine la conexión para devolver el saldo a Ahorro."; +"lightning__move_to_spark__too_large_smaller" = "Puede moverlo en partes más pequeñas."; +"lightning__move_to_spark__too_large_text" = "{amount} es demasiado grande para enrutarse a Spark en un solo pago."; +"lightning__move_to_spark__too_large_title" = "Cantidad demasiado grande"; +"lightning__move_to_spark__try" = "Mover {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Comisión de reclamo"; "lightning__spark_confirmations" = "{count}/3 confirmaciones"; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index 4074d833e..48b53a2d3 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -346,6 +346,30 @@ "lightning__transaction" = "Transaction"; "lightning__order_fee" = "Frais"; "lightning__balance" = "Solde"; +"lightning__move_to_spark__amount" = "Montant"; +"lightning__move_to_spark__close" = "Fermer la connexion"; +"lightning__move_to_spark__confirm_text" = "Votre solde passe à Spark via Lightning, le moyen le moins coûteux de le garder dans Dépenses. Les frais de routage ci-dessous sont le maximum que vous payez."; +"lightning__move_to_spark__confirm_title" = "Déplacer vers Spark"; +"lightning__move_to_spark__done_rest_text" = "Une partie de votre solde est maintenant sur Spark. Déplacez le reste de la même façon, puis fermez la connexion vidée."; +"lightning__move_to_spark__done_text" = "Votre solde est maintenant sur Spark. Fermez la connexion vidée pour rendre sa réserve à Épargnes."; +"lightning__move_to_spark__done_title" = "Déplacé vers Spark"; +"lightning__move_to_spark__failed_nothing" = "Vos connexions Lightning n'ont plus de solde à déplacer."; +"lightning__move_to_spark__failed_spark" = "Spark n'est pas encore prêt à recevoir. Réessayez dans un instant."; +"lightning__move_to_spark__failed_title" = "Échec du déplacement"; +"lightning__move_to_spark__failed_unknown" = "Un problème est survenu pendant la préparation du déplacement. Réessayez."; +"lightning__move_to_spark__fee" = "Frais de routage"; +"lightning__move_to_spark__in_flight_text" = "Le paiement est en route et apparaîtra sur Spark une fois réglé. Fermez ensuite la connexion vidée pour rendre sa réserve à Épargnes."; +"lightning__move_to_spark__intro_text" = "Blocktank ferme ses connexions Lightning. Déplacez votre solde vers Spark maintenant : c'est l'option la moins coûteuse, et il reste dans Dépenses."; +"lightning__move_to_spark__intro_title" = "Votre connexion se ferme"; +"lightning__move_to_spark__move_rest" = "Déplacer le reste"; +"lightning__move_to_spark__nav_title" = "Déplacer vers Spark"; +"lightning__move_to_spark__quoting" = "Recherche d'un itinéraire et de ses frais…"; +"lightning__move_to_spark__swipe" = "Glissez pour déplacer"; +"lightning__move_to_spark__too_large_none" = "Fermez plutôt la connexion pour rendre le solde à Épargnes."; +"lightning__move_to_spark__too_large_smaller" = "Vous pouvez le déplacer en plus petites parties."; +"lightning__move_to_spark__too_large_text" = "{amount} est trop élevé pour être acheminé vers Spark en un seul paiement."; +"lightning__move_to_spark__too_large_title" = "Montant trop élevé"; +"lightning__move_to_spark__try" = "Déplacer {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Frais de réclamation"; "lightning__spark_confirmations" = "{count}/3 confirmations"; diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings index ff531fab8..0a1724b03 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -339,6 +339,30 @@ "lightning__transaction" = "Transazione"; "lightning__order_fee" = "Fee Ordine"; "lightning__balance" = "Saldo"; +"lightning__move_to_spark__amount" = "Importo"; +"lightning__move_to_spark__close" = "Chiudi Connessione"; +"lightning__move_to_spark__confirm_text" = "Il tuo saldo passa a Spark tramite Lightning, il modo a commissioni più basse per mantenerlo in Spesa. La commissione di instradamento qui sotto è il massimo che paghi."; +"lightning__move_to_spark__confirm_title" = "Sposta su Spark"; +"lightning__move_to_spark__done_rest_text" = "Una parte del tuo saldo è ora su Spark. Sposta il resto allo stesso modo, poi chiudi la connessione svuotata."; +"lightning__move_to_spark__done_text" = "Il tuo saldo è ora su Spark. Chiudi la connessione svuotata per restituire la sua riserva ai Risparmi."; +"lightning__move_to_spark__done_title" = "Spostato su Spark"; +"lightning__move_to_spark__failed_nothing" = "Le tue connessioni Lightning non hanno più saldo da spostare."; +"lightning__move_to_spark__failed_spark" = "Spark non è ancora pronto a ricevere. Riprova tra un momento."; +"lightning__move_to_spark__failed_title" = "Impossibile spostare"; +"lightning__move_to_spark__failed_unknown" = "Qualcosa è andato storto durante la preparazione dello spostamento. Riprova."; +"lightning__move_to_spark__fee" = "Commissione di instradamento"; +"lightning__move_to_spark__in_flight_text" = "Il pagamento è in arrivo e compare su Spark una volta completato. Poi chiudi la connessione svuotata per restituire la sua riserva ai Risparmi."; +"lightning__move_to_spark__intro_text" = "Blocktank sta chiudendo le sue connessioni Lightning. Sposta ora il tuo saldo su Spark: è l'opzione con le commissioni più basse e resta in Spesa."; +"lightning__move_to_spark__intro_title" = "La tua connessione si sta chiudendo"; +"lightning__move_to_spark__move_rest" = "Sposta Il Resto"; +"lightning__move_to_spark__nav_title" = "Sposta Su Spark"; +"lightning__move_to_spark__quoting" = "Ricerca di un percorso e della sua commissione…"; +"lightning__move_to_spark__swipe" = "Swipe Per Spostare"; +"lightning__move_to_spark__too_large_none" = "Chiudi invece la connessione per restituire il saldo ai Risparmi."; +"lightning__move_to_spark__too_large_smaller" = "Puoi spostarlo in parti più piccole."; +"lightning__move_to_spark__too_large_text" = "{amount} è troppo grande per essere instradato su Spark in un unico pagamento."; +"lightning__move_to_spark__too_large_title" = "Importo troppo grande"; +"lightning__move_to_spark__try" = "Sposta {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Commissione di riscatto"; "lightning__spark_confirmations" = "{count}/3 conferme"; diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings index 087be4fee..734d7a6e5 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -343,6 +343,30 @@ "lightning__transaction" = "Transactie"; "lightning__order_fee" = "Order kosten"; "lightning__balance" = "Saldo"; +"lightning__move_to_spark__amount" = "Bedrag"; +"lightning__move_to_spark__close" = "Verbinding sluiten"; +"lightning__move_to_spark__confirm_text" = "Uw saldo gaat via Lightning naar Spark, de goedkoopste manier om het op uw bestedingssaldo te houden. De routeringsvergoeding hieronder is het maximum dat u betaalt."; +"lightning__move_to_spark__confirm_title" = "Verplaatsen naar Spark"; +"lightning__move_to_spark__done_rest_text" = "Een deel van uw saldo staat nu op Spark. Verplaats de rest op dezelfde manier en sluit daarna de lege verbinding."; +"lightning__move_to_spark__done_text" = "Uw saldo staat nu op Spark. Sluit de lege verbinding om de reserve terug te geven aan uw spaargeld."; +"lightning__move_to_spark__done_title" = "Verplaatst naar Spark"; +"lightning__move_to_spark__failed_nothing" = "Uw Lightning-verbindingen hebben geen saldo meer om te verplaatsen."; +"lightning__move_to_spark__failed_spark" = "Spark is nog niet klaar om te ontvangen. Probeer het zo meteen opnieuw."; +"lightning__move_to_spark__failed_title" = "Verplaatsen mislukt"; +"lightning__move_to_spark__failed_unknown" = "Er is iets misgegaan bij het voorbereiden van de verplaatsing. Probeer het opnieuw."; +"lightning__move_to_spark__fee" = "Routeringsvergoeding"; +"lightning__move_to_spark__in_flight_text" = "De betaling is onderweg en verschijnt in Spark zodra ze is afgerond. Sluit daarna de lege verbinding om de reserve terug te geven aan uw spaargeld."; +"lightning__move_to_spark__intro_text" = "Blocktank sluit zijn Lightning-verbindingen. Verplaats uw saldo nu naar Spark: dit is de goedkoopste optie en het blijft op uw bestedingssaldo."; +"lightning__move_to_spark__intro_title" = "Uw verbinding wordt gesloten"; +"lightning__move_to_spark__move_rest" = "De rest verplaatsen"; +"lightning__move_to_spark__nav_title" = "Verplaatsen naar Spark"; +"lightning__move_to_spark__quoting" = "Route en vergoeding zoeken…"; +"lightning__move_to_spark__swipe" = "Veeg om te verplaatsen"; +"lightning__move_to_spark__too_large_none" = "Sluit in plaats daarvan de verbinding om het saldo terug te geven aan uw spaargeld."; +"lightning__move_to_spark__too_large_smaller" = "U kunt het in kleinere delen verplaatsen."; +"lightning__move_to_spark__too_large_text" = "{amount} is te groot om in één betaling naar Spark te routeren."; +"lightning__move_to_spark__too_large_title" = "Bedrag te groot"; +"lightning__move_to_spark__try" = "{amount} verplaatsen"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Claimkosten"; "lightning__spark_confirmations" = "{count}/3 bevestigingen"; diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings index 1cd7e31c2..69ddd7a76 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -345,6 +345,30 @@ "lightning__transaction" = "Transakcja"; "lightning__order_fee" = "Opłata za zamówienie"; "lightning__balance" = "Saldo"; +"lightning__move_to_spark__amount" = "Kwota"; +"lightning__move_to_spark__close" = "Zamknij połączenie"; +"lightning__move_to_spark__confirm_text" = "Twoje saldo trafia do Spark przez Lightning, najtańszym sposobem na pozostawienie go w Wydatkach. Opłata za routing poniżej to maksimum, jakie zapłacisz."; +"lightning__move_to_spark__confirm_title" = "Przenieś do Spark"; +"lightning__move_to_spark__done_rest_text" = "Część Twojego salda jest teraz w Spark. Przenieś resztę w ten sam sposób, a następnie zamknij opróżnione połączenie."; +"lightning__move_to_spark__done_text" = "Twoje saldo jest teraz w Spark. Zamknij opróżnione połączenie, aby zwrócić jego rezerwę do Oszczędności."; +"lightning__move_to_spark__done_title" = "Przeniesiono do Spark"; +"lightning__move_to_spark__failed_nothing" = "Twoje połączenia Lightning nie mają już salda do przeniesienia."; +"lightning__move_to_spark__failed_spark" = "Spark nie jest jeszcze gotowy do odbioru. Spróbuj ponownie za chwilę."; +"lightning__move_to_spark__failed_title" = "Nie udało się przenieść"; +"lightning__move_to_spark__failed_unknown" = "Coś poszło nie tak podczas przygotowania przeniesienia. Spróbuj ponownie."; +"lightning__move_to_spark__fee" = "Opłata za routing"; +"lightning__move_to_spark__in_flight_text" = "Płatność jest w drodze i pojawi się w Spark po rozliczeniu. Następnie zamknij opróżnione połączenie, aby zwrócić jego rezerwę do Oszczędności."; +"lightning__move_to_spark__intro_text" = "Blocktank zamyka swoje połączenia Lightning. Przenieś teraz saldo do Spark: to opcja z najniższymi opłatami, a środki zostają w Wydatkach."; +"lightning__move_to_spark__intro_title" = "Twoje połączenie się zamyka"; +"lightning__move_to_spark__move_rest" = "Przenieś resztę"; +"lightning__move_to_spark__nav_title" = "Przenieś do Spark"; +"lightning__move_to_spark__quoting" = "Szukanie trasy i jej opłaty…"; +"lightning__move_to_spark__swipe" = "Przesuń, aby przenieść"; +"lightning__move_to_spark__too_large_none" = "Zamiast tego zamknij połączenie, aby zwrócić saldo do Oszczędności."; +"lightning__move_to_spark__too_large_smaller" = "Możesz przenieść je w mniejszych częściach."; +"lightning__move_to_spark__too_large_text" = "Kwota {amount} jest zbyt duża, aby przekazać ją do Spark jedną płatnością."; +"lightning__move_to_spark__too_large_title" = "Kwota zbyt duża"; +"lightning__move_to_spark__try" = "Przenieś {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Opłata za odbiór"; "lightning__spark_confirmations" = "{count}/3 potwierdzeń"; diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings index bd54df0b4..7673ef186 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -345,6 +345,30 @@ "lightning__transaction" = "Transação"; "lightning__order_fee" = "Taxa de pedido"; "lightning__balance" = "Saldo"; +"lightning__move_to_spark__amount" = "Valor"; +"lightning__move_to_spark__close" = "Fechar Conexão"; +"lightning__move_to_spark__confirm_text" = "Seu saldo vai para o Spark pela Lightning, a forma de menor taxa de mantê-lo em Gastos. A taxa de roteamento abaixo é o máximo que você paga."; +"lightning__move_to_spark__confirm_title" = "Mover para o Spark"; +"lightning__move_to_spark__done_rest_text" = "Parte do seu saldo agora está no Spark. Mova o resto da mesma forma e depois feche a conexão esvaziada."; +"lightning__move_to_spark__done_text" = "Seu saldo agora está no Spark. Feche a conexão esvaziada para devolver a reserva à Poupança."; +"lightning__move_to_spark__done_title" = "Movido para o Spark"; +"lightning__move_to_spark__failed_nothing" = "Suas conexões Lightning não têm mais saldo para mover."; +"lightning__move_to_spark__failed_spark" = "O Spark ainda não está pronto para receber. Tente novamente em instantes."; +"lightning__move_to_spark__failed_title" = "Não foi possível mover"; +"lightning__move_to_spark__failed_unknown" = "Algo deu errado ao preparar a movimentação. Tente novamente."; +"lightning__move_to_spark__fee" = "Taxa de roteamento"; +"lightning__move_to_spark__in_flight_text" = "O pagamento está a caminho e aparece no Spark assim que for liquidado. Depois, feche a conexão esvaziada para devolver a reserva à Poupança."; +"lightning__move_to_spark__intro_text" = "O Blocktank está fechando suas conexões Lightning. Mova seu saldo para o Spark agora: é a opção de menor taxa e ele continua em Gastos."; +"lightning__move_to_spark__intro_title" = "Sua conexão está fechando"; +"lightning__move_to_spark__move_rest" = "Mover o Resto"; +"lightning__move_to_spark__nav_title" = "Mover Para o Spark"; +"lightning__move_to_spark__quoting" = "Buscando uma rota e sua taxa…"; +"lightning__move_to_spark__swipe" = "Deslizar Para Mover"; +"lightning__move_to_spark__too_large_none" = "Feche a conexão para devolver o saldo à Poupança."; +"lightning__move_to_spark__too_large_smaller" = "Você pode movê-lo em partes menores."; +"lightning__move_to_spark__too_large_text" = "{amount} é grande demais para ser roteado ao Spark em um único pagamento."; +"lightning__move_to_spark__too_large_title" = "Valor grande demais"; +"lightning__move_to_spark__try" = "Mover {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Taxa de resgate"; "lightning__spark_confirmations" = "{count}/3 confirmações"; diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings index 5ab90bafd..69c618da8 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -343,6 +343,30 @@ "lightning__transaction" = "Transação"; "lightning__order_fee" = "Taxa de pedido"; "lightning__balance" = "Saldo"; +"lightning__move_to_spark__amount" = "Montante"; +"lightning__move_to_spark__close" = "Fechar Conexão"; +"lightning__move_to_spark__confirm_text" = "O seu saldo passa para o Spark através do Lightning, a forma com as taxas mais baixas de o manter em Gastos. A taxa de encaminhamento abaixo é o máximo que paga."; +"lightning__move_to_spark__confirm_title" = "Mover para o Spark"; +"lightning__move_to_spark__done_rest_text" = "Parte do seu saldo está agora no Spark. Mova o resto da mesma forma e depois feche a conexão esvaziada."; +"lightning__move_to_spark__done_text" = "O seu saldo está agora no Spark. Feche a conexão esvaziada para devolver a reserva à Poupança."; +"lightning__move_to_spark__done_title" = "Movido para o Spark"; +"lightning__move_to_spark__failed_nothing" = "As suas conexões Lightning já não têm saldo para mover."; +"lightning__move_to_spark__failed_spark" = "O Spark ainda não está pronto para receber. Tente novamente dentro de instantes."; +"lightning__move_to_spark__failed_title" = "Não foi possível mover"; +"lightning__move_to_spark__failed_unknown" = "Algo correu mal ao preparar a movimentação. Tente novamente."; +"lightning__move_to_spark__fee" = "Taxa de encaminhamento"; +"lightning__move_to_spark__in_flight_text" = "O pagamento está a caminho e aparece no Spark quando for liquidado. Depois, feche a conexão esvaziada para devolver a reserva à Poupança."; +"lightning__move_to_spark__intro_text" = "O Blocktank está a fechar as suas conexões Lightning. Mova já o seu saldo para o Spark: é a opção com as taxas mais baixas e fica em Gastos."; +"lightning__move_to_spark__intro_title" = "A sua conexão está a fechar"; +"lightning__move_to_spark__move_rest" = "Mover o Resto"; +"lightning__move_to_spark__nav_title" = "Mover Para o Spark"; +"lightning__move_to_spark__quoting" = "A procurar uma rota e a respetiva taxa…"; +"lightning__move_to_spark__swipe" = "Deslizar Para Mover"; +"lightning__move_to_spark__too_large_none" = "Feche antes a conexão para devolver o saldo à Poupança."; +"lightning__move_to_spark__too_large_smaller" = "Pode movê-lo em partes mais pequenas."; +"lightning__move_to_spark__too_large_text" = "{amount} é demasiado grande para ser encaminhado para o Spark num só pagamento."; +"lightning__move_to_spark__too_large_title" = "Montante demasiado grande"; +"lightning__move_to_spark__try" = "Mover {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Taxa de resgate"; "lightning__spark_confirmations" = "{count}/3 confirmações"; diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings index d2df40a5b..665e89f75 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -340,6 +340,30 @@ "lightning__transaction" = "Транзакция"; "lightning__order_fee" = "Комиссия Ордера"; "lightning__balance" = "Баланс"; +"lightning__move_to_spark__amount" = "Сумма"; +"lightning__move_to_spark__close" = "Закрыть Соединение"; +"lightning__move_to_spark__confirm_text" = "Ваш баланс переходит в Spark через Lightning — самым дешёвым по комиссиям способом сохранить его в Расходах. Комиссия за маршрутизацию ниже — максимум, который вы платите."; +"lightning__move_to_spark__confirm_title" = "Перенос в Spark"; +"lightning__move_to_spark__done_rest_text" = "Часть вашего баланса теперь в Spark. Перенесите остаток таким же способом, затем закройте опустевшее соединение."; +"lightning__move_to_spark__done_text" = "Ваш баланс теперь в Spark. Закройте опустевшее соединение, чтобы вернуть его резерв в Сбережения."; +"lightning__move_to_spark__done_title" = "Перенесено в Spark"; +"lightning__move_to_spark__failed_nothing" = "На ваших соединениях Lightning больше нет баланса для переноса."; +"lightning__move_to_spark__failed_spark" = "Spark пока не готов к получению. Попробуйте ещё раз чуть позже."; +"lightning__move_to_spark__failed_title" = "Не удалось перенести"; +"lightning__move_to_spark__failed_unknown" = "Что-то пошло не так при подготовке переноса. Попробуйте ещё раз."; +"lightning__move_to_spark__fee" = "Комиссия за маршрутизацию"; +"lightning__move_to_spark__in_flight_text" = "Платёж в пути и появится в Spark после завершения. Затем закройте опустевшее соединение, чтобы вернуть его резерв в Сбережения."; +"lightning__move_to_spark__intro_text" = "Blocktank закрывает свои соединения Lightning. Перенесите баланс в Spark сейчас: это вариант с самой низкой комиссией, и средства остаются в Расходах."; +"lightning__move_to_spark__intro_title" = "Ваше соединение закрывается"; +"lightning__move_to_spark__move_rest" = "Перенести Остаток"; +"lightning__move_to_spark__nav_title" = "Перенос в Spark"; +"lightning__move_to_spark__quoting" = "Поиск маршрута и его комиссии…"; +"lightning__move_to_spark__swipe" = "Проведите для Переноса"; +"lightning__move_to_spark__too_large_none" = "Вместо этого закройте соединение, чтобы вернуть баланс в Сбережения."; +"lightning__move_to_spark__too_large_smaller" = "Вы можете перенести его меньшими частями."; +"lightning__move_to_spark__too_large_text" = "{amount} — слишком большая сумма для маршрутизации в Spark одним платежом."; +"lightning__move_to_spark__too_large_title" = "Сумма слишком велика"; +"lightning__move_to_spark__try" = "Перенести {amount}"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Комиссия за зачисление"; "lightning__spark_confirmations" = "{count}/3 подтверждений"; diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index 97b7f4d40..0badc4556 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -936,13 +936,16 @@ class LightningService: NodeLifecycleService { } } + /// `channelsOnly` pays from the channels and never falls back to the channel-free rail, as moving a channel + /// balance to Spark needs. func send( bolt11: String, sats: UInt64? = nil, params: RouteParametersConfig? = nil, + channelsOnly: Bool = false, beforeSubmission: @escaping () throws -> Void = {} ) async throws -> PaymentHash { - if let rail = channelFreeRail, await shouldPayWithRail(rail, bolt11: bolt11, sats: sats) { + if !channelsOnly, let rail = channelFreeRail, await shouldPayWithRail(rail, bolt11: bolt11, sats: sats) { // Spark takes no route parameters, so a caller's routing fee cap is checked against its estimate instead. if let maxFeeMsat = params?.maxTotalRoutingFeeMsat { let feeSats: UInt64 @@ -1898,8 +1901,8 @@ extension LightningService { } } - func estimateRoutingFees(bolt11: String, amountSats: UInt64? = nil) async throws -> UInt64 { - if let rail = channelFreeRail, await shouldPayWithRail(rail, bolt11: bolt11, sats: amountSats) { + func estimateRoutingFees(bolt11: String, amountSats: UInt64? = nil, channelsOnly: Bool = false) async throws -> UInt64 { + if !channelsOnly, let rail = channelFreeRail, await shouldPayWithRail(rail, bolt11: bolt11, sats: amountSats) { return try await rail.estimateRoutingFees(bolt11: bolt11, amountSats: amountSats) } guard let node else { diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index ce446259f..3b1508557 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -207,6 +207,7 @@ class AppViewModel: ObservableObject { @AppStorage("hasSeenProfileIntro") var hasSeenProfileIntro: Bool = false @AppStorage("hasSeenNotificationsIntro") var hasSeenNotificationsIntro: Bool = false @AppStorage("hasSeenQuickpayIntro") var hasSeenQuickpayIntro: Bool = false + @AppStorage("hasSeenMoveToSparkIntro") var hasSeenMoveToSparkIntro: Bool = false @AppStorage("hasSeenShopIntro") var hasSeenShopIntro: Bool = false @AppStorage("hasSeenTransferIntro") var hasSeenTransferIntro: Bool = false @AppStorage("hasSeenTransferToSpendingIntro") var hasSeenTransferToSpendingIntro: Bool = false @@ -560,6 +561,7 @@ class AppViewModel: ObservableObject { hasSeenProfileIntro = false hasSeenNotificationsIntro = false hasSeenQuickpayIntro = false + hasSeenMoveToSparkIntro = false hasSeenShopIntro = false hasSeenTransferIntro = false hasSeenTransferToSpendingIntro = false @@ -1779,7 +1781,11 @@ extension AppViewModel { func handleLdkNodeEvent(_ event: Event) { switch event { - case let .paymentReceived(paymentId, _, amountMsat, _): + case let .paymentReceived(paymentId, paymentHash, amountMsat, _): + // A move to Spark reports its outcome on its own screen. + if MoveToSparkPayments.contains(paymentHash) { + break + } Task { if let paymentId { if await CoreService.shared.activity.isActivitySeen(id: paymentId) { diff --git a/Bitkit/ViewModels/NavigationViewModel.swift b/Bitkit/ViewModels/NavigationViewModel.swift index 5bae41280..1d1ad44a4 100644 --- a/Bitkit/ViewModels/NavigationViewModel.swift +++ b/Bitkit/ViewModels/NavigationViewModel.swift @@ -104,6 +104,7 @@ enum Route: Hashable { case connections case connectionDetail(channelId: String) case sparkConnectionDetail + case moveToSpark case closeConnection(channel: ChannelDetails) case node case electrumSettings diff --git a/Bitkit/ViewModels/SheetViewModel.swift b/Bitkit/ViewModels/SheetViewModel.swift index b94d45bfd..728459caf 100644 --- a/Bitkit/ViewModels/SheetViewModel.swift +++ b/Bitkit/ViewModels/SheetViewModel.swift @@ -17,6 +17,7 @@ enum SheetID: String, CaseIterable { case subscription case quickpay case usdtIntro + case moveToSpark case receive case receivedTx case btcpayConnection @@ -395,6 +396,18 @@ class SheetViewModel: ObservableObject { } } + var moveToSparkSheetItem: MoveToSparkSheetItem? { + get { + guard activeSheetConfiguration?.id == .moveToSpark else { return nil } + return MoveToSparkSheetItem() + } + set { + if newValue == nil { + activeSheetConfiguration = nil + } + } + } + var quickpaySheetItem: QuickpaySheetItem? { get { guard let config = activeSheetConfiguration, config.id == .quickpay else { return nil } diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 1c36919dd..26b9f061f 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -1156,6 +1156,19 @@ class WalletViewModel: ObservableObject { return try await watchSend(hash: String(hash)) } + /// Pays from the channels only, with the routing fee capped at `maxFeeMsat`, and waits for the payment to settle. + func sendOverChannels(bolt11: String, sats: UInt64, maxFeeMsat: UInt64) async throws -> SettledLightningPayment { + // LDK's default route parameters, with the routing fee capped. + let params = RouteParametersConfig( + maxTotalRoutingFeeMsat: maxFeeMsat, + maxTotalCltvExpiryDelta: 1008, + maxPathCount: 10, + maxChannelSaturationPowerOfHalf: 2 + ) + let hash = try await lightningService.send(bolt11: bolt11, sats: sats, params: params, channelsOnly: true) + return try await watchSend(hash: String(hash)) + } + /// Observes task cancellation so `waitForLightningPayment`'s task group can unwind after its /// timeout instead of awaiting a continuation that only an LDK event would resume. private func watchSend(hash: String) async throws -> SettledLightningPayment { diff --git a/Bitkit/Views/HomeScreen.swift b/Bitkit/Views/HomeScreen.swift index 54bd62d78..fb7e2b1dc 100644 --- a/Bitkit/Views/HomeScreen.swift +++ b/Bitkit/Views/HomeScreen.swift @@ -9,6 +9,10 @@ struct HomeScreen: View { @EnvironmentObject var settings: SettingsViewModel @EnvironmentObject var wallet: WalletViewModel + private var canMoveToSpark: Bool { + MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: SparkManager.shared.isReady && !SparkManager.shared.isExiting) + } + @State private var scrollPosition: Int? = 0 @State private var isEditingWidgets = false @State private var pullRefreshState = HomePullRefreshState() @@ -103,6 +107,12 @@ struct HomeScreen: View { .onDisappear { TimedSheetManager.shared.onPrimaryScreenExited() } + // Channels and Spark load after Home first checks its sheets, so the Move to Spark intro is checked again. + .onChange(of: canMoveToSpark) { _, canMove in + if canMove { + TimedSheetManager.shared.reevaluate() + } + } .onChange(of: app.requestedHomePage) { _, _ in consumeRequestedHomePage() } diff --git a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift index 35e4605b9..fba0b981c 100644 --- a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift +++ b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift @@ -71,6 +71,17 @@ struct LightningConnectionsView: View { EmptyStateView() .padding(.top, 16) } else { + if MoveToSparkMath.canMove( + channels: wallet.channels, + isSparkUsable: SparkManager.shared.isReady && !SparkManager.shared.isExiting + ) { + CustomButton(title: t("lightning__move_to_spark__nav_title")) { + navigation.navigate(.moveToSpark) + } + .accessibilityIdentifier("MoveToSpark") + .padding(.vertical, 16) + } + if SparkConnectionRow.isVisible { SettingsSectionHeader(t("lightning__spark")) SparkConnectionRow() diff --git a/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift b/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift new file mode 100644 index 000000000..87c3d6c81 --- /dev/null +++ b/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift @@ -0,0 +1,165 @@ +import LDKNode +import SwiftUI + +/// Moves the channels' balance to the user's Spark wallet over Lightning, then offers to close the emptied channel. +struct MoveToSparkView: View { + @EnvironmentObject var currency: CurrencyViewModel + @EnvironmentObject var navigation: NavigationViewModel + @EnvironmentObject var wallet: WalletViewModel + @State private var manager = MoveToSparkManager() + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavigationBar(title: t("lightning__move_to_spark__nav_title")) + .padding(.bottom, 16) + + switch manager.state { + case .quoting: + quoting + case let .confirm(quote, isMoving): + confirm(quote: quote, isMoving: isMoving) + case let .done(amountSats, feeSats, closeChannel, hasRest): + done(amountSats: amountSats, feeSats: feeSats, closeChannel: closeChannel, hasRest: hasRest) + case let .tooLarge(amountSats, smallerSats): + tooLarge(amountSats: amountSats, smallerSats: smallerSats) + case let .failed(failure): + failed(failure) + } + } + .navigationBarHidden(true) + .padding(.horizontal, 16) + .bottomSafeAreaPadding() + .task { + await manager.quote(wallet: wallet) + } + } + + private func header(title: String, text: String) -> some View { + VStack(alignment: .leading, spacing: 16) { + DisplayText(title, accentColor: .purpleAccent) + BodyMText(text) + .fixedSize(horizontal: false, vertical: true) + } + } + + private func illustration(_ name: String) -> some View { + Image(name) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 256, maxHeight: 256) + .frame(maxWidth: .infinity) + .frame(maxHeight: .infinity) + } + + private var quoting: some View { + VStack(alignment: .leading, spacing: 0) { + header(title: t("lightning__move_to_spark__confirm_title"), text: t("lightning__move_to_spark__quoting")) + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + + private func confirm(quote: MoveToSparkQuote, isMoving: Bool) -> some View { + VStack(alignment: .leading, spacing: 0) { + header(title: t("lightning__move_to_spark__confirm_title"), text: t("lightning__move_to_spark__confirm_text")) + .padding(.bottom, 32) + SparkExitAmountRow(label: t("lightning__move_to_spark__amount"), sats: quote.amountSats) + .accessibilityIdentifier("MoveToSparkAmount") + SparkExitAmountRow(label: t("lightning__move_to_spark__fee"), sats: quote.feeSats) + .accessibilityIdentifier("MoveToSparkFee") + illustration("lightning") + SwipeButton(title: t("lightning__move_to_spark__swipe"), accentColor: .purpleAccent, isLoading: isMoving) { + await manager.move(wallet: wallet) + } + .accessibilityIdentifier("MoveToSparkSwipe") + } + } + + private func done(amountSats: UInt64, feeSats: UInt64, closeChannel: ChannelDetails?, hasRest: Bool) -> some View { + VStack(alignment: .leading, spacing: 0) { + header( + title: t("lightning__move_to_spark__done_title"), + text: hasRest ? t("lightning__move_to_spark__done_rest_text") : t("lightning__move_to_spark__done_text") + ) + .padding(.bottom, 32) + SparkExitAmountRow(label: t("lightning__move_to_spark__amount"), sats: amountSats) + .accessibilityIdentifier("MoveToSparkMoved") + SparkExitAmountRow(label: t("lightning__move_to_spark__fee"), sats: feeSats) + .accessibilityIdentifier("MoveToSparkFeePaid") + illustration("check") + HStack(spacing: 16) { + CustomButton(title: t("common__ok"), variant: .secondary) { + navigation.navigateBack() + } + if hasRest { + CustomButton(title: t("lightning__move_to_spark__move_rest")) { + await manager.quote(wallet: wallet) + } + .accessibilityIdentifier("MoveToSparkMoveRest") + } else { + CustomButton(title: t("lightning__move_to_spark__close")) { + // Closing returns to Lightning Connections through the channel's detail, as a close from there does. + navigation.navigateBack() + navigation.navigate(.connections) + if let closeChannel { + navigation.navigate(.connectionDetail(channelId: closeChannel.channelIdString)) + navigation.navigate(.closeConnection(channel: closeChannel)) + } + } + .accessibilityIdentifier("MoveToSparkClose") + } + } + } + } + + private func tooLarge(amountSats: UInt64, smallerSats: UInt64?) -> some View { + let next = smallerSats == nil ? t("lightning__move_to_spark__too_large_none") : t("lightning__move_to_spark__too_large_smaller") + let amount = t("lightning__move_to_spark__too_large_text", variables: ["amount": currency.primaryAmountText(sats: amountSats)]) + return VStack(alignment: .leading, spacing: 0) { + header(title: t("lightning__move_to_spark__too_large_title"), text: "\(amount) \(next)") + illustration("exclamation-mark") + if let smallerSats { + HStack(spacing: 16) { + CustomButton(title: t("common__cancel"), variant: .secondary) { + navigation.navigateBack() + } + CustomButton(title: t("lightning__move_to_spark__try", variables: ["amount": currency.primaryAmountText(sats: smallerSats)])) { + await manager.quote(wallet: wallet, targetSats: smallerSats) + } + .accessibilityIdentifier("MoveToSparkTrySmaller") + } + } else { + CustomButton(title: t("common__back")) { + navigation.navigateBack() + } + } + } + } + + private func failed(_ failure: MoveToSparkFailure) -> some View { + let text = switch failure { + case .nothingToMove: t("lightning__move_to_spark__failed_nothing") + case .sparkUnavailable: t("lightning__move_to_spark__failed_spark") + case .unknown: t("lightning__move_to_spark__failed_unknown") + } + return VStack(alignment: .leading, spacing: 0) { + header(title: t("lightning__move_to_spark__failed_title"), text: text) + illustration("cross") + if failure == .nothingToMove { + CustomButton(title: t("common__back")) { + navigation.navigateBack() + } + } else { + HStack(spacing: 16) { + CustomButton(title: t("common__cancel"), variant: .secondary) { + navigation.navigateBack() + } + CustomButton(title: t("common__try_again")) { + await manager.quote(wallet: wallet) + } + .accessibilityIdentifier("MoveToSparkRetry") + } + } + } + } +} diff --git a/Bitkit/Views/Sheets/MoveToSparkSheet.swift b/Bitkit/Views/Sheets/MoveToSparkSheet.swift new file mode 100644 index 000000000..dd775a94b --- /dev/null +++ b/Bitkit/Views/Sheets/MoveToSparkSheet.swift @@ -0,0 +1,57 @@ +import SwiftUI + +struct MoveToSparkSheetItem: SheetItem { + let id: SheetID = .moveToSpark + let size: SheetSize = .large +} + +/// Tells channel holders that Blocktank closes its channels and that moving the balance to Spark costs least. +struct MoveToSparkSheet: View { + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + @EnvironmentObject private var navigation: NavigationViewModel + @EnvironmentObject private var sheets: SheetViewModel + let config: MoveToSparkSheetItem + + var body: some View { + Sheet(id: .moveToSpark, data: config) { + VStack(alignment: .leading, spacing: 0) { + SheetHeader(title: t("lightning__move_to_spark__nav_title")) + GeometryReader { geometry in + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 32) { + Image("lightning") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 256, height: 256) + .frame(maxWidth: .infinity) + VStack(alignment: .leading, spacing: 8) { + DisplayText(t("lightning__move_to_spark__intro_title"), accentColor: .purpleAccent) + BodyMText(t("lightning__move_to_spark__intro_text")) + .fixedSize(horizontal: false, vertical: true) + } + } + .frame(minHeight: geometry.size.height, alignment: .bottom) + } + } + let buttonLayout = dynamicTypeSize.isAccessibilitySize + ? AnyLayout(VStackLayout(spacing: 16)) + : AnyLayout(HStackLayout(spacing: 16)) + buttonLayout { + CustomButton(title: t("common__later"), variant: .secondary, shouldExpand: true) { + sheets.hideSheet() + } + .accessibilityIdentifier("MoveToSparkIntroLater") + CustomButton(title: t("lightning__move_to_spark__nav_title"), shouldExpand: true) { + sheets.hideSheet() + navigation.navigate(.moveToSpark) + } + .accessibilityIdentifier("MoveToSparkIntroMove") + } + .padding(.top, 32) + } + .padding(.horizontal, 16) + .accessibilityElement(children: .contain) + .accessibilityIdentifier("MoveToSparkIntro") + } + } +} diff --git a/Bitkit/Views/Wallets/SpendingWalletScreen.swift b/Bitkit/Views/Wallets/SpendingWalletScreen.swift index 3e8522614..7a5381403 100644 --- a/Bitkit/Views/Wallets/SpendingWalletScreen.swift +++ b/Bitkit/Views/Wallets/SpendingWalletScreen.swift @@ -7,6 +7,10 @@ struct SpendingWalletScreen: View { @EnvironmentObject var sheets: SheetViewModel @EnvironmentObject var wallet: WalletViewModel + private var canMoveToSpark: Bool { + MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: SparkManager.shared.isReady && !SparkManager.shared.isExiting) + } + private var shouldShowOnboarding: Bool { let hasLightningActivities = activity.lightningActivities?.isEmpty == false return wallet.totalLightningSats == 0 && !hasLightningActivities @@ -41,6 +45,14 @@ struct SpendingWalletScreen: View { } if !shouldShowOnboarding { + if canMoveToSpark { + CustomButton(title: t("lightning__move_to_spark__nav_title")) { + navigation.navigate(.moveToSpark) + } + .accessibilityIdentifier("MoveToSpark") + .padding(.top, 28) + } + if wallet.channels?.isEmpty == false || SparkManager.shared.balanceSats > 0 { transferButton .transition(.move(edge: .leading).combined(with: .opacity)) diff --git a/BitkitTests/MoveToSparkMathTests.swift b/BitkitTests/MoveToSparkMathTests.swift new file mode 100644 index 000000000..0f90725f9 --- /dev/null +++ b/BitkitTests/MoveToSparkMathTests.swift @@ -0,0 +1,33 @@ +@testable import Bitkit +import XCTest + +final class MoveToSparkMathTests: XCTestCase { + func testFirstCandidateLeavesRoomForAGuessedFee() { + let amount = MoveToSparkMath.firstCandidate(wantedSats: 100_000, limitSats: 100_000) + XCTAssertLessThan(amount, 100_000) + XCTAssertEqual(amount, 100_000 - 500 - 2) + } + + func testFirstCandidateKeepsASmallerTarget() { + XCTAssertEqual(MoveToSparkMath.firstCandidate(wantedSats: 50000, limitSats: 100_000), 50000) + } + + func testAmountAfterFeeFitsUnderTheLimit() { + XCTAssertEqual(MoveToSparkMath.amountAfterFee(limitSats: 1_000_000, feeSats: 9000), 990_998) + XCTAssertEqual(MoveToSparkMath.amountAfterFee(limitSats: 5, feeSats: 9000), 0) + } + + func testFeeCapCoversTheQuotedFeeAndItsMargin() { + XCTAssertEqual(MoveToSparkMath.feeCapMsat(feeSats: 30), 32000) + } + + func testSmallerAmountHalvesUntilTheMinimum() { + XCTAssertEqual(MoveToSparkMath.smallerAmount(100_000), 50000) + XCTAssertNil(MoveToSparkMath.smallerAmount(MoveToSparkMath.minMoveSats)) + } + + func testMoveIsOfferedOnlyWithAUsableSpark() { + XCTAssertFalse(MoveToSparkMath.canMove(channels: [], isSparkUsable: true)) + XCTAssertFalse(MoveToSparkMath.canMove(channels: nil, isSparkUsable: false)) + } +} diff --git a/changelog.d/next/1206.added.md b/changelog.d/next/1206.added.md new file mode 100644 index 000000000..4562661bb --- /dev/null +++ b/changelog.d/next/1206.added.md @@ -0,0 +1 @@ +Move to Spark: move a Lightning connection's balance to Spark at the lowest fee, then close the emptied connection to return its reserve to Savings. diff --git a/journeys/transfer/move-to-spark.xml b/journeys/transfer/move-to-spark.xml new file mode 100644 index 000000000..83e2502e0 --- /dev/null +++ b/journeys/transfer/move-to-spark.xml @@ -0,0 +1,29 @@ + + + Verifies the post-update "Move to Spark" sheet and the move itself. With an LDK channel that can send at least + 1,000 sats and Spark connected, Home shows the "Move To Spark" sheet once (testTag "MoveToSparkIntro"); Spending + and Lightning Connections show a "Move To Spark" button (testTag "MoveToSpark") while the channel can still send. + The move quotes the routing fee over the channel only, pays the amount to a new invoice of the user's own Spark + wallet, and then offers to close the emptied connection. A wallet without such a channel never gets the sheet or + the button. + + The "too large to route" state (testTag "MoveToSparkTrySmaller") needs a route that cannot carry the balance; on a + direct channel to the Spark service provider every amount routes, so it is covered by MoveToSparkRepoTest and the + MoveToSparkScreen previews rather than here. + + Precondition: dev build pointed at a local Spark regtest (spark-sdk regtest/local), and a wallet with an open + channel to that stack's SSP Lightning node that holds most of its capacity on the wallet's side (open it from + Lightning Connections, Manual Setup, with the node URI the stack prints). Start on the wallet home screen with the + app freshly launched. + + + Wait on Home and verify the "Move To Spark" sheet (testTag "MoveToSparkIntro") shows, saying Blocktank is closing its Lightning connections + Tap "Later" (testTag "MoveToSparkIntroLater") and verify the sheet closes + Relaunch the app, wait on Home and verify the sheet does not show again + Open the Spending wallet and verify the "Move To Spark" button (testTag "MoveToSpark") shows + Tap "Move To Spark" and verify the screen shows an amount (testTag "MoveToSparkAmount") and a routing fee (testTag "MoveToSparkFee") + Swipe to move (testTag "MoveToSparkSwipe") and verify "Moved to Spark" shows the moved amount (testTag "MoveToSparkMoved") and "Close Connection" (testTag "MoveToSparkClose") + Tap "Close Connection", then "Close" on the close confirmation, and verify Lightning Connections shows no open connection and the Spark balance grew by the moved amount + Go back to Home, open Spending and verify the "Move To Spark" button is gone + + From 46533445fcbe9089aea880d834606a621ed2ab1d Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 19:20:40 +0100 Subject: [PATCH 2/9] test: keep the move to spark journey platform neutral --- journeys/transfer/move-to-spark.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/journeys/transfer/move-to-spark.xml b/journeys/transfer/move-to-spark.xml index 83e2502e0..4a47e1307 100644 --- a/journeys/transfer/move-to-spark.xml +++ b/journeys/transfer/move-to-spark.xml @@ -8,8 +8,8 @@ the button. The "too large to route" state (testTag "MoveToSparkTrySmaller") needs a route that cannot carry the balance; on a - direct channel to the Spark service provider every amount routes, so it is covered by MoveToSparkRepoTest and the - MoveToSparkScreen previews rather than here. + direct channel to the Spark service provider every amount routes, so it is covered by the quote's unit tests and the + PR's manual test rather than here. Precondition: dev build pointed at a local Spark regtest (spark-sdk regtest/local), and a wallet with an open channel to that stack's SSP Lightning node that holds most of its capacity on the wallet's side (open it from From 25d82e8ecfce8a8433c39ec77ccca2e14b61b1a0 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 19:29:22 +0100 Subject: [PATCH 3/9] fix: list move to spark after the channels in connections --- .../Advanced/LightningConnectionsView.swift | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift index fba0b981c..cbef45199 100644 --- a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift +++ b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift @@ -71,17 +71,6 @@ struct LightningConnectionsView: View { EmptyStateView() .padding(.top, 16) } else { - if MoveToSparkMath.canMove( - channels: wallet.channels, - isSparkUsable: SparkManager.shared.isReady && !SparkManager.shared.isExiting - ) { - CustomButton(title: t("lightning__move_to_spark__nav_title")) { - navigation.navigate(.moveToSpark) - } - .accessibilityIdentifier("MoveToSpark") - .padding(.vertical, 16) - } - if SparkConnectionRow.isVisible { SettingsSectionHeader(t("lightning__spark")) SparkConnectionRow() @@ -166,6 +155,17 @@ struct LightningConnectionsView: View { } } + if MoveToSparkMath.canMove( + channels: wallet.channels, + isSparkUsable: SparkManager.shared.isReady && !SparkManager.shared.isExiting + ) { + CustomButton(title: t("lightning__move_to_spark__nav_title")) { + navigation.navigate(.moveToSpark) + } + .accessibilityIdentifier("MoveToSpark") + .padding(.vertical, 16) + } + // Closed Connections section if showClosedConnections && !closedChannels.isEmpty { SettingsSectionHeader(t("lightning__conn_closed")) From ada7ad68ca20f046f1495979a6c51429509da647 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 20:23:51 +0100 Subject: [PATCH 4/9] fix: fit move to spark labels and headlines to small screens --- .../Localization/ar.lproj/Localizable.strings | 14 +++++++------- .../Localization/ca.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/cs.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/de.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/el.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/en.lproj/Localizable.strings | 16 ++++++++-------- .../es-419.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/es.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/fr.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/it.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/nl.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/pl.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/pt-BR.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/pt.lproj/Localizable.strings | 16 ++++++++-------- .../Localization/ru.lproj/Localizable.strings | 16 ++++++++-------- .../Settings/Advanced/MoveToSparkView.swift | 6 +++--- Bitkit/Views/Sheets/MoveToSparkSheet.swift | 2 +- 17 files changed, 123 insertions(+), 123 deletions(-) diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index 8292254a8..2b25b4605 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -293,20 +293,21 @@ "lightning__order_fee" = "رسوم الطلب"; "lightning__balance" = "الرصيد"; "lightning__move_to_spark__amount" = "المبلغ"; -"lightning__move_to_spark__close" = "إغلاق الاتصال"; "lightning__move_to_spark__confirm_text" = "ينتقل رصيدك إلى Spark عبر Lightning، وهي أقل طريقة رسومًا للإبقاء عليه في الإنفاق. رسوم التوجيه أدناه هي أقصى ما تدفعه."; -"lightning__move_to_spark__confirm_title" = "الانتقال إلى Spark"; +"lightning__move_to_spark__confirm_title" = "الانتقال إلى\nSpark"; "lightning__move_to_spark__done_rest_text" = "جزء من رصيدك الآن على Spark. انقل الباقي بالطريقة نفسها، ثم أغلق الاتصال الذي أصبح فارغًا."; "lightning__move_to_spark__done_text" = "رصيدك الآن على Spark. أغلق الاتصال الذي أصبح فارغًا لإعادة احتياطيه إلى المدخرات."; -"lightning__move_to_spark__done_title" = "تم النقل إلى Spark"; +"lightning__move_to_spark__done_title" = "تم النقل إلى\nSpark"; "lightning__move_to_spark__failed_nothing" = "لم يتبق في اتصالات Lightning لديك رصيد للنقل."; "lightning__move_to_spark__failed_spark" = "Spark غير جاهز للاستلام بعد. حاول مرة أخرى بعد قليل."; -"lightning__move_to_spark__failed_title" = "تعذّر النقل"; +"lightning__move_to_spark__failed_title" = "تعذّر إتمام\nالنقل"; "lightning__move_to_spark__failed_unknown" = "حدث خطأ أثناء تحضير النقل. حاول مرة أخرى."; "lightning__move_to_spark__fee" = "رسوم التوجيه"; "lightning__move_to_spark__in_flight_text" = "الدفعة في طريقها وستظهر في Spark بمجرد إتمامها. ثم أغلق الاتصال الفارغ لإعادة احتياطيه إلى المدخرات."; "lightning__move_to_spark__intro_text" = "يقوم Blocktank بإغلاق اتصالات Lightning الخاصة به. انقل رصيدك إلى Spark الآن: فهو الخيار الأقل رسومًا، ويبقى ضمن الإنفاق."; -"lightning__move_to_spark__intro_title" = "اتصالك قيد الإغلاق"; +"lightning__move_to_spark__intro_title" = "اتصالك على وشك\nالإغلاق"; +"lightning__move_to_spark__move_half" = "نقل النصف"; +"lightning__move_to_spark__move_now" = "نقل الآن"; "lightning__move_to_spark__move_rest" = "نقل الباقي"; "lightning__move_to_spark__nav_title" = "النقل إلى Spark"; "lightning__move_to_spark__quoting" = "جارٍ البحث عن مسار ورسومه…"; @@ -314,8 +315,7 @@ "lightning__move_to_spark__too_large_none" = "أغلق الاتصال بدلاً من ذلك لإعادة الرصيد إلى المدخرات."; "lightning__move_to_spark__too_large_smaller" = "يمكنك نقله على دفعات أصغر."; "lightning__move_to_spark__too_large_text" = "{amount} أكبر من أن يُوجَّه إلى Spark في دفعة واحدة."; -"lightning__move_to_spark__too_large_title" = "المبلغ كبير جدًا"; -"lightning__move_to_spark__try" = "نقل {amount}"; +"lightning__move_to_spark__too_large_title" = "المبلغ كبير\nجدًا"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "رسوم المطالبة"; "lightning__spark_confirmations" = "{count}/3 تأكيدات"; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index a3c35f2e6..792e69310 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -330,29 +330,29 @@ "lightning__order_fee" = "Comissió de comanda"; "lightning__balance" = "Saldo"; "lightning__move_to_spark__amount" = "Import"; -"lightning__move_to_spark__close" = "Tanca la connexió"; "lightning__move_to_spark__confirm_text" = "El teu saldo passa a Spark per Lightning, la manera amb menys tarifes de mantenir-lo a Despesa. La tarifa d'enrutament de sota és el màxim que pagues."; -"lightning__move_to_spark__confirm_title" = "Moure a Spark"; +"lightning__move_to_spark__confirm_title" = "Moure a\nSpark"; "lightning__move_to_spark__done_rest_text" = "Una part del teu saldo ja és a Spark. Mou la resta de la mateixa manera i després tanca la connexió buida."; "lightning__move_to_spark__done_text" = "El teu saldo ja és a Spark. Tanca la connexió buida per tornar-ne la reserva als Estalvis."; -"lightning__move_to_spark__done_title" = "Mogut a Spark"; +"lightning__move_to_spark__done_title" = "Mogut a\nSpark"; "lightning__move_to_spark__failed_nothing" = "Les teves connexions Lightning ja no tenen saldo per moure."; "lightning__move_to_spark__failed_spark" = "Spark encara no està a punt per rebre. Torna-ho a provar d'aquí a un moment."; -"lightning__move_to_spark__failed_title" = "No s'ha pogut moure"; +"lightning__move_to_spark__failed_title" = "No s'ha pogut\nmoure"; "lightning__move_to_spark__failed_unknown" = "Alguna cosa ha anat malament en preparar el moviment. Torna-ho a provar."; "lightning__move_to_spark__fee" = "Tarifa d'enrutament"; "lightning__move_to_spark__in_flight_text" = "El pagament està en camí i apareixerà a Spark quan s'hagi liquidat. Després tanca la connexió buida per tornar-ne la reserva als Estalvis."; "lightning__move_to_spark__intro_text" = "Blocktank està tancant les seves connexions Lightning. Mou ara el teu saldo a Spark: és l'opció amb menys tarifes i es manté a Despesa."; -"lightning__move_to_spark__intro_title" = "La teva connexió es tanca"; -"lightning__move_to_spark__move_rest" = "Moure la resta"; +"lightning__move_to_spark__intro_title" = "La teva connexió\nes tanca"; +"lightning__move_to_spark__move_half" = "Moure meitat"; +"lightning__move_to_spark__move_now" = "Moure ara"; +"lightning__move_to_spark__move_rest" = "Moure resta"; "lightning__move_to_spark__nav_title" = "Moure a Spark"; "lightning__move_to_spark__quoting" = "Cercant una ruta i la seva tarifa…"; "lightning__move_to_spark__swipe" = "Desplaça per moure"; "lightning__move_to_spark__too_large_none" = "Tanca la connexió per tornar el saldo als Estalvis."; "lightning__move_to_spark__too_large_smaller" = "El pots moure en parts més petites."; "lightning__move_to_spark__too_large_text" = "{amount} és massa gran per enrutar-se a Spark en un sol pagament."; -"lightning__move_to_spark__too_large_title" = "Import massa gran"; -"lightning__move_to_spark__try" = "Moure {amount}"; +"lightning__move_to_spark__too_large_title" = "Import massa\ngran"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Comissió de reclamació"; "lightning__spark_confirmations" = "{count}/3 confirmacions"; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index 09fb959fc..27b9677e0 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -346,29 +346,29 @@ "lightning__order_fee" = "Poplatek za objednávku"; "lightning__balance" = "Zůstatek"; "lightning__move_to_spark__amount" = "Částka"; -"lightning__move_to_spark__close" = "Uzavřít spojení"; "lightning__move_to_spark__confirm_text" = "Váš zůstatek se přesune do Spark přes Lightning, což je způsob s nejnižšími poplatky, jak jej udržet v Útratách. Poplatek za směrování níže je maximum, které zaplatíte."; -"lightning__move_to_spark__confirm_title" = "Přesun do Spark"; +"lightning__move_to_spark__confirm_title" = "Přesun do\nSpark"; "lightning__move_to_spark__done_rest_text" = "Část vašeho zůstatku je nyní ve Spark. Zbytek přesuňte stejným způsobem a poté uzavřete vyprázdněné spojení."; "lightning__move_to_spark__done_text" = "Váš zůstatek je nyní ve Spark. Uzavřete vyprázdněné spojení, aby se jeho rezerva vrátila do Úspor."; -"lightning__move_to_spark__done_title" = "Přesunuto do Spark"; +"lightning__move_to_spark__done_title" = "Přesunuto do\nSpark"; "lightning__move_to_spark__failed_nothing" = "Vaše spojení Lightning už nemají žádný zůstatek k přesunu."; "lightning__move_to_spark__failed_spark" = "Spark zatím není připraven přijímat. Zkuste to za chvíli znovu."; -"lightning__move_to_spark__failed_title" = "Přesun se nezdařil"; +"lightning__move_to_spark__failed_title" = "Přesun se\nnezdařil"; "lightning__move_to_spark__failed_unknown" = "Při přípravě přesunu se něco pokazilo. Zkuste to znovu."; "lightning__move_to_spark__fee" = "Poplatek za směrování"; "lightning__move_to_spark__in_flight_text" = "Platba je na cestě a ve Spark se zobrazí po vypořádání. Poté uzavřete vyprázdněné spojení, aby se jeho rezerva vrátila do Úspor."; "lightning__move_to_spark__intro_text" = "Blocktank uzavírá svá spojení Lightning. Přesuňte svůj zůstatek do Spark hned: je to možnost s nejnižšími poplatky a zůstane v Útratách."; -"lightning__move_to_spark__intro_title" = "Vaše spojení se uzavírá"; -"lightning__move_to_spark__move_rest" = "Přesunout zbytek"; +"lightning__move_to_spark__intro_title" = "Vaše spojení\nse uzavírá"; +"lightning__move_to_spark__move_half" = "Přesun půlku"; +"lightning__move_to_spark__move_now" = "Přesun hned"; +"lightning__move_to_spark__move_rest" = "Zbytek"; "lightning__move_to_spark__nav_title" = "Přesun do Spark"; "lightning__move_to_spark__quoting" = "Hledání trasy a jejího poplatku…"; "lightning__move_to_spark__swipe" = "Přejetím přesuňte"; "lightning__move_to_spark__too_large_none" = "Místo toho uzavřete spojení, aby se zůstatek vrátil do Úspor."; "lightning__move_to_spark__too_large_smaller" = "Můžete jej přesunout po menších částech."; "lightning__move_to_spark__too_large_text" = "{amount} je příliš velká částka na směrování do Spark v jedné platbě."; -"lightning__move_to_spark__too_large_title" = "Částka je příliš velká"; -"lightning__move_to_spark__try" = "Přesunout {amount}"; +"lightning__move_to_spark__too_large_title" = "Částka je příliš\nvelká"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Poplatek za připsání"; "lightning__spark_confirmations" = "{count}/3 potvrzení"; diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings index e6159b281..bc1567b02 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -344,29 +344,29 @@ "lightning__order_fee" = "Auftragsgebühr"; "lightning__balance" = "Guthaben"; "lightning__move_to_spark__amount" = "Betrag"; -"lightning__move_to_spark__close" = "Verbindung schließen"; "lightning__move_to_spark__confirm_text" = "Ihr Guthaben wechselt über Lightning zu Spark, dem günstigsten Weg, es im Ausgabenkonto zu behalten. Die Routing-Gebühr unten ist das Maximum, das Sie zahlen."; -"lightning__move_to_spark__confirm_title" = "Zu Spark verschieben"; +"lightning__move_to_spark__confirm_title" = "Verschieben zu\nSpark"; "lightning__move_to_spark__done_rest_text" = "Ein Teil Ihres Guthabens ist jetzt bei Spark. Verschieben Sie den Rest auf dieselbe Weise und schließen Sie dann die geleerte Verbindung."; "lightning__move_to_spark__done_text" = "Ihr Guthaben ist jetzt bei Spark. Schließen Sie die geleerte Verbindung, um deren Reserve an das Sparkonto zurückzugeben."; -"lightning__move_to_spark__done_title" = "Zu Spark verschoben"; +"lightning__move_to_spark__done_title" = "Verschoben zu\nSpark"; "lightning__move_to_spark__failed_nothing" = "Ihre Lightning-Verbindungen haben kein Guthaben mehr zum Verschieben."; "lightning__move_to_spark__failed_spark" = "Spark ist noch nicht bereit zum Empfangen. Versuchen Sie es gleich noch einmal."; -"lightning__move_to_spark__failed_title" = "Verschieben fehlgeschlagen"; +"lightning__move_to_spark__failed_title" = "Das Verschieben\nschlug fehl"; "lightning__move_to_spark__failed_unknown" = "Beim Vorbereiten der Verschiebung ist etwas schiefgelaufen. Versuchen Sie es erneut."; "lightning__move_to_spark__fee" = "Routing-Gebühr"; "lightning__move_to_spark__in_flight_text" = "Die Zahlung ist unterwegs und erscheint in Spark, sobald sie abgeschlossen ist. Schließen Sie dann die geleerte Verbindung, um deren Reserve an das Sparkonto zurückzugeben."; "lightning__move_to_spark__intro_text" = "Blocktank schließt seine Lightning-Verbindungen. Verschieben Sie Ihr Guthaben jetzt zu Spark: Das ist die günstigste Option, und es bleibt im Ausgabenkonto."; -"lightning__move_to_spark__intro_title" = "Ihre Verbindung wird geschlossen"; -"lightning__move_to_spark__move_rest" = "Rest verschieben"; +"lightning__move_to_spark__intro_title" = "Ihre Verbindung wird\ngeschlossen"; +"lightning__move_to_spark__move_half" = "Halb senden"; +"lightning__move_to_spark__move_now" = "Jetzt senden"; +"lightning__move_to_spark__move_rest" = "Rest senden"; "lightning__move_to_spark__nav_title" = "Zu Spark verschieben"; "lightning__move_to_spark__quoting" = "Route und Gebühr werden gesucht…"; "lightning__move_to_spark__swipe" = "Swipe zum Verschieben"; "lightning__move_to_spark__too_large_none" = "Schließen Sie stattdessen die Verbindung, um das Guthaben an das Sparkonto zurückzugeben."; "lightning__move_to_spark__too_large_smaller" = "Sie können es in kleineren Teilen verschieben."; "lightning__move_to_spark__too_large_text" = "{amount} ist zu groß, um in einer Zahlung zu Spark geroutet zu werden."; -"lightning__move_to_spark__too_large_title" = "Betrag zu groß"; -"lightning__move_to_spark__try" = "{amount} verschieben"; +"lightning__move_to_spark__too_large_title" = "Betrag ist\nzu groß"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Einlösegebühr"; "lightning__spark_confirmations" = "{count}/3 Bestätigungen"; diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings index 282d46583..bbb5cabf6 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -317,29 +317,29 @@ "lightning__order_fee" = "Τέλος παραγγελίας"; "lightning__balance" = "Υπόλοιπο"; "lightning__move_to_spark__amount" = "Ποσό"; -"lightning__move_to_spark__close" = "Κλείσιμο σύνδεσης"; "lightning__move_to_spark__confirm_text" = "Το υπόλοιπό σας μεταφέρεται στο Spark μέσω Lightning, τον τρόπο με τα χαμηλότερα τέλη για να παραμείνει στις Δαπάνες. Το τέλος δρομολόγησης παρακάτω είναι το μέγιστο που πληρώνετε."; -"lightning__move_to_spark__confirm_title" = "Μεταφορά στο Spark"; +"lightning__move_to_spark__confirm_title" = "Μεταφορά στο\nSpark"; "lightning__move_to_spark__done_rest_text" = "Ένα μέρος του υπολοίπου σας βρίσκεται πλέον στο Spark. Μεταφέρετε τα υπόλοιπα με τον ίδιο τρόπο και έπειτα κλείστε την άδεια σύνδεση."; "lightning__move_to_spark__done_text" = "Το υπόλοιπό σας βρίσκεται πλέον στο Spark. Κλείστε την άδεια σύνδεση για να επιστρέψει το απόθεμά της στις Αποταμιεύσεις."; -"lightning__move_to_spark__done_title" = "Μεταφέρθηκε στο Spark"; +"lightning__move_to_spark__done_title" = "Μεταφέρθηκε στο\nSpark"; "lightning__move_to_spark__failed_nothing" = "Οι συνδέσεις Lightning σας δεν έχουν πλέον υπόλοιπο για μεταφορά."; "lightning__move_to_spark__failed_spark" = "Το Spark δεν είναι ακόμη έτοιμο να λάβει. Δοκιμάστε ξανά σε λίγο."; -"lightning__move_to_spark__failed_title" = "Αποτυχία μεταφοράς"; +"lightning__move_to_spark__failed_title" = "Η μεταφορά\nαπέτυχε"; "lightning__move_to_spark__failed_unknown" = "Κάτι πήγε στραβά κατά την προετοιμασία της μεταφοράς. Δοκιμάστε ξανά."; "lightning__move_to_spark__fee" = "Τέλος δρομολόγησης"; "lightning__move_to_spark__in_flight_text" = "Η πληρωμή βρίσκεται σε εξέλιξη και θα εμφανιστεί στο Spark μόλις διακανονιστεί. Έπειτα κλείστε την άδεια σύνδεση για να επιστρέψει το απόθεμά της στις Αποταμιεύσεις."; "lightning__move_to_spark__intro_text" = "Το Blocktank κλείνει τις συνδέσεις Lightning του. Μεταφέρετε το υπόλοιπό σας στο Spark τώρα: είναι η επιλογή με τα χαμηλότερα τέλη και παραμένει στις Δαπάνες."; -"lightning__move_to_spark__intro_title" = "Η σύνδεσή σας κλείνει"; -"lightning__move_to_spark__move_rest" = "Μεταφορά των υπολοίπων"; +"lightning__move_to_spark__intro_title" = "Η σύνδεσή σας\nκλείνει"; +"lightning__move_to_spark__move_half" = "Το μισό"; +"lightning__move_to_spark__move_now" = "Μεταφορά"; +"lightning__move_to_spark__move_rest" = "Τα υπόλοιπα"; "lightning__move_to_spark__nav_title" = "Μεταφορά στο Spark"; "lightning__move_to_spark__quoting" = "Εύρεση διαδρομής και του τέλους της…"; "lightning__move_to_spark__swipe" = "Σύρε για μεταφορά"; "lightning__move_to_spark__too_large_none" = "Κλείστε αντί αυτού τη σύνδεση για να επιστρέψει το υπόλοιπο στις Αποταμιεύσεις."; "lightning__move_to_spark__too_large_smaller" = "Μπορείτε να το μεταφέρετε σε μικρότερα μέρη."; "lightning__move_to_spark__too_large_text" = "Το {amount} είναι πολύ μεγάλο για δρομολόγηση στο Spark με μία πληρωμή."; -"lightning__move_to_spark__too_large_title" = "Το ποσό είναι πολύ μεγάλο"; -"lightning__move_to_spark__try" = "Μεταφορά {amount}"; +"lightning__move_to_spark__too_large_title" = "Το ποσό είναι\nπολύ μεγάλο"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Χρέωση εξαργύρωσης"; "lightning__spark_confirmations" = "{count}/3 επιβεβαιώσεις"; diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index fb59f88ab..290f12f1c 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -339,29 +339,29 @@ "lightning__order_fee" = "Order fee"; "lightning__balance" = "Balance"; "lightning__move_to_spark__amount" = "Amount"; -"lightning__move_to_spark__close" = "Close Connection"; "lightning__move_to_spark__confirm_text" = "Your balance moves to Spark over Lightning, the lowest-fee way to keep it in Spending. The routing fee below is the most you pay."; -"lightning__move_to_spark__confirm_title" = "Move to Spark"; +"lightning__move_to_spark__confirm_title" = "Move to\nSpark"; "lightning__move_to_spark__done_rest_text" = "Part of your balance is on Spark now. Move the rest the same way, then close the emptied connection."; "lightning__move_to_spark__done_text" = "Your balance is on Spark now. Close the emptied connection to return its reserve to Savings."; -"lightning__move_to_spark__done_title" = "Moved to Spark"; +"lightning__move_to_spark__done_title" = "Moved to\nSpark"; "lightning__move_to_spark__failed_nothing" = "Your Lightning connections have no balance left to move."; "lightning__move_to_spark__failed_spark" = "Spark is not ready to receive yet. Try again in a moment."; -"lightning__move_to_spark__failed_title" = "Could not move"; +"lightning__move_to_spark__failed_title" = "Could not\nmove"; "lightning__move_to_spark__failed_unknown" = "Something went wrong while preparing the move. Try again."; "lightning__move_to_spark__fee" = "Routing fee"; "lightning__move_to_spark__in_flight_text" = "The payment is on its way and shows in Spark once it settles. Then close the emptied connection to return its reserve to Savings."; "lightning__move_to_spark__intro_text" = "Blocktank is closing its Lightning connections. Move your balance to Spark now: it is the lowest-fee option, and it stays in Spending."; -"lightning__move_to_spark__intro_title" = "Your connection is closing"; -"lightning__move_to_spark__move_rest" = "Move The Rest"; +"lightning__move_to_spark__intro_title" = "Your connection\nis closing"; +"lightning__move_to_spark__move_half" = "Move Half"; +"lightning__move_to_spark__move_now" = "Move Now"; +"lightning__move_to_spark__move_rest" = "Move Rest"; "lightning__move_to_spark__nav_title" = "Move To Spark"; "lightning__move_to_spark__quoting" = "Finding a route and its fee…"; "lightning__move_to_spark__swipe" = "Swipe To Move"; "lightning__move_to_spark__too_large_none" = "Close the connection instead to return the balance to Savings."; "lightning__move_to_spark__too_large_smaller" = "You can move it in smaller parts."; "lightning__move_to_spark__too_large_text" = "{amount} is too large to route to Spark in one payment."; -"lightning__move_to_spark__too_large_title" = "Amount too large"; -"lightning__move_to_spark__try" = "Move {amount}"; +"lightning__move_to_spark__too_large_title" = "Amount\ntoo large"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Claim fee"; "lightning__spark_confirmations" = "{count}/3 confirmations"; diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings index bc525c9af..e60b8669b 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -347,29 +347,29 @@ "lightning__order_fee" = "Tarifa de pedido"; "lightning__balance" = "Saldo"; "lightning__move_to_spark__amount" = "Monto"; -"lightning__move_to_spark__close" = "Terminar conexión"; "lightning__move_to_spark__confirm_text" = "Su saldo pasa a Spark por Lightning, la forma de menor comisión para mantenerlo en Gasto. La comisión de enrutamiento de abajo es lo máximo que paga."; -"lightning__move_to_spark__confirm_title" = "Mover a Spark"; +"lightning__move_to_spark__confirm_title" = "Mover a\nSpark"; "lightning__move_to_spark__done_rest_text" = "Una parte de su saldo ya está en Spark. Mueva el resto de la misma forma y luego termine la conexión vacía."; "lightning__move_to_spark__done_text" = "Su saldo ya está en Spark. Termine la conexión vacía para devolver su reserva a Ahorro."; -"lightning__move_to_spark__done_title" = "Movido a Spark"; +"lightning__move_to_spark__done_title" = "Movido a\nSpark"; "lightning__move_to_spark__failed_nothing" = "Sus conexiones Lightning ya no tienen saldo para mover."; "lightning__move_to_spark__failed_spark" = "Spark aún no está listo para recibir. Inténtelo de nuevo en un momento."; -"lightning__move_to_spark__failed_title" = "No se pudo mover"; +"lightning__move_to_spark__failed_title" = "No se pudo\nmover"; "lightning__move_to_spark__failed_unknown" = "Algo salió mal al preparar el movimiento. Inténtelo de nuevo."; "lightning__move_to_spark__fee" = "Comisión de enrutamiento"; "lightning__move_to_spark__in_flight_text" = "El pago está en camino y aparecerá en Spark cuando se complete. Luego termine la conexión vacía para devolver su reserva a Ahorro."; "lightning__move_to_spark__intro_text" = "Blocktank está cerrando sus conexiones Lightning. Mueva su saldo a Spark ahora: es la opción de menor comisión y se mantiene en Gasto."; -"lightning__move_to_spark__intro_title" = "Su conexión se está cerrando"; -"lightning__move_to_spark__move_rest" = "Mover el resto"; +"lightning__move_to_spark__intro_title" = "Su conexión\nse cierra"; +"lightning__move_to_spark__move_half" = "Mover mitad"; +"lightning__move_to_spark__move_now" = "Mover ahora"; +"lightning__move_to_spark__move_rest" = "Mover resto"; "lightning__move_to_spark__nav_title" = "Mover a Spark"; "lightning__move_to_spark__quoting" = "Buscando una ruta y su comisión…"; "lightning__move_to_spark__swipe" = "Deslizar para mover"; "lightning__move_to_spark__too_large_none" = "Termine la conexión para devolver el saldo a Ahorro."; "lightning__move_to_spark__too_large_smaller" = "Puede moverlo en partes más pequeñas."; "lightning__move_to_spark__too_large_text" = "{amount} es demasiado grande para enrutarse a Spark en un solo pago."; -"lightning__move_to_spark__too_large_title" = "Monto demasiado grande"; -"lightning__move_to_spark__try" = "Mover {amount}"; +"lightning__move_to_spark__too_large_title" = "El monto es\nexcesivo"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Comisión de reclamo"; "lightning__spark_confirmations" = "{count}/3 confirmaciones"; diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings index ce6cd6080..643c37078 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -331,29 +331,29 @@ "lightning__order_fee" = "Tarifa de pedido"; "lightning__balance" = "Saldo"; "lightning__move_to_spark__amount" = "Cantidad"; -"lightning__move_to_spark__close" = "Terminar conexión"; "lightning__move_to_spark__confirm_text" = "Su saldo pasa a Spark por Lightning, la forma de menor comisión para mantenerlo en Gasto. La comisión de enrutamiento de abajo es lo máximo que paga."; -"lightning__move_to_spark__confirm_title" = "Mover a Spark"; +"lightning__move_to_spark__confirm_title" = "Mover a\nSpark"; "lightning__move_to_spark__done_rest_text" = "Una parte de su saldo ya está en Spark. Mueva el resto de la misma forma y luego termine la conexión vacía."; "lightning__move_to_spark__done_text" = "Su saldo ya está en Spark. Termine la conexión vacía para devolver su reserva a Ahorro."; -"lightning__move_to_spark__done_title" = "Movido a Spark"; +"lightning__move_to_spark__done_title" = "Movido a\nSpark"; "lightning__move_to_spark__failed_nothing" = "Sus conexiones Lightning ya no tienen saldo para mover."; "lightning__move_to_spark__failed_spark" = "Spark aún no está listo para recibir. Inténtelo de nuevo en un momento."; -"lightning__move_to_spark__failed_title" = "No se pudo mover"; +"lightning__move_to_spark__failed_title" = "No se pudo\nmover"; "lightning__move_to_spark__failed_unknown" = "Algo salió mal al preparar el movimiento. Inténtelo de nuevo."; "lightning__move_to_spark__fee" = "Comisión de enrutamiento"; "lightning__move_to_spark__in_flight_text" = "El pago está en camino y aparecerá en Spark cuando se complete. Luego termine la conexión vacía para devolver su reserva a Ahorro."; "lightning__move_to_spark__intro_text" = "Blocktank está cerrando sus conexiones Lightning. Mueva su saldo a Spark ahora: es la opción de menor comisión y se mantiene en Gasto."; -"lightning__move_to_spark__intro_title" = "Su conexión se está cerrando"; -"lightning__move_to_spark__move_rest" = "Mover el resto"; +"lightning__move_to_spark__intro_title" = "Su conexión\nse cierra"; +"lightning__move_to_spark__move_half" = "Mover mitad"; +"lightning__move_to_spark__move_now" = "Mover ahora"; +"lightning__move_to_spark__move_rest" = "Mover resto"; "lightning__move_to_spark__nav_title" = "Mover a Spark"; "lightning__move_to_spark__quoting" = "Buscando una ruta y su comisión…"; "lightning__move_to_spark__swipe" = "Deslizar para mover"; "lightning__move_to_spark__too_large_none" = "Termine la conexión para devolver el saldo a Ahorro."; "lightning__move_to_spark__too_large_smaller" = "Puede moverlo en partes más pequeñas."; "lightning__move_to_spark__too_large_text" = "{amount} es demasiado grande para enrutarse a Spark en un solo pago."; -"lightning__move_to_spark__too_large_title" = "Cantidad demasiado grande"; -"lightning__move_to_spark__try" = "Mover {amount}"; +"lightning__move_to_spark__too_large_title" = "La cantidad es\nexcesiva"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Comisión de reclamo"; "lightning__spark_confirmations" = "{count}/3 confirmaciones"; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index 48b53a2d3..f9e31ea35 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -347,29 +347,29 @@ "lightning__order_fee" = "Frais"; "lightning__balance" = "Solde"; "lightning__move_to_spark__amount" = "Montant"; -"lightning__move_to_spark__close" = "Fermer la connexion"; "lightning__move_to_spark__confirm_text" = "Votre solde passe à Spark via Lightning, le moyen le moins coûteux de le garder dans Dépenses. Les frais de routage ci-dessous sont le maximum que vous payez."; -"lightning__move_to_spark__confirm_title" = "Déplacer vers Spark"; +"lightning__move_to_spark__confirm_title" = "Déplacer vers\nSpark"; "lightning__move_to_spark__done_rest_text" = "Une partie de votre solde est maintenant sur Spark. Déplacez le reste de la même façon, puis fermez la connexion vidée."; "lightning__move_to_spark__done_text" = "Votre solde est maintenant sur Spark. Fermez la connexion vidée pour rendre sa réserve à Épargnes."; -"lightning__move_to_spark__done_title" = "Déplacé vers Spark"; +"lightning__move_to_spark__done_title" = "Déplacé vers\nSpark"; "lightning__move_to_spark__failed_nothing" = "Vos connexions Lightning n'ont plus de solde à déplacer."; "lightning__move_to_spark__failed_spark" = "Spark n'est pas encore prêt à recevoir. Réessayez dans un instant."; -"lightning__move_to_spark__failed_title" = "Échec du déplacement"; +"lightning__move_to_spark__failed_title" = "Le déplacement\na échoué"; "lightning__move_to_spark__failed_unknown" = "Un problème est survenu pendant la préparation du déplacement. Réessayez."; "lightning__move_to_spark__fee" = "Frais de routage"; "lightning__move_to_spark__in_flight_text" = "Le paiement est en route et apparaîtra sur Spark une fois réglé. Fermez ensuite la connexion vidée pour rendre sa réserve à Épargnes."; "lightning__move_to_spark__intro_text" = "Blocktank ferme ses connexions Lightning. Déplacez votre solde vers Spark maintenant : c'est l'option la moins coûteuse, et il reste dans Dépenses."; -"lightning__move_to_spark__intro_title" = "Votre connexion se ferme"; -"lightning__move_to_spark__move_rest" = "Déplacer le reste"; +"lightning__move_to_spark__intro_title" = "Votre connexion\nse ferme"; +"lightning__move_to_spark__move_half" = "Moitié"; +"lightning__move_to_spark__move_now" = "Déplacer"; +"lightning__move_to_spark__move_rest" = "Le reste"; "lightning__move_to_spark__nav_title" = "Déplacer vers Spark"; "lightning__move_to_spark__quoting" = "Recherche d'un itinéraire et de ses frais…"; "lightning__move_to_spark__swipe" = "Glissez pour déplacer"; "lightning__move_to_spark__too_large_none" = "Fermez plutôt la connexion pour rendre le solde à Épargnes."; "lightning__move_to_spark__too_large_smaller" = "Vous pouvez le déplacer en plus petites parties."; "lightning__move_to_spark__too_large_text" = "{amount} est trop élevé pour être acheminé vers Spark en un seul paiement."; -"lightning__move_to_spark__too_large_title" = "Montant trop élevé"; -"lightning__move_to_spark__try" = "Déplacer {amount}"; +"lightning__move_to_spark__too_large_title" = "Le montant est\ntrop élevé"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Frais de réclamation"; "lightning__spark_confirmations" = "{count}/3 confirmations"; diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings index 0a1724b03..a30b36f4f 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -340,29 +340,29 @@ "lightning__order_fee" = "Fee Ordine"; "lightning__balance" = "Saldo"; "lightning__move_to_spark__amount" = "Importo"; -"lightning__move_to_spark__close" = "Chiudi Connessione"; "lightning__move_to_spark__confirm_text" = "Il tuo saldo passa a Spark tramite Lightning, il modo a commissioni più basse per mantenerlo in Spesa. La commissione di instradamento qui sotto è il massimo che paghi."; -"lightning__move_to_spark__confirm_title" = "Sposta su Spark"; +"lightning__move_to_spark__confirm_title" = "Sposta su\nSpark"; "lightning__move_to_spark__done_rest_text" = "Una parte del tuo saldo è ora su Spark. Sposta il resto allo stesso modo, poi chiudi la connessione svuotata."; "lightning__move_to_spark__done_text" = "Il tuo saldo è ora su Spark. Chiudi la connessione svuotata per restituire la sua riserva ai Risparmi."; -"lightning__move_to_spark__done_title" = "Spostato su Spark"; +"lightning__move_to_spark__done_title" = "Spostato su\nSpark"; "lightning__move_to_spark__failed_nothing" = "Le tue connessioni Lightning non hanno più saldo da spostare."; "lightning__move_to_spark__failed_spark" = "Spark non è ancora pronto a ricevere. Riprova tra un momento."; -"lightning__move_to_spark__failed_title" = "Impossibile spostare"; +"lightning__move_to_spark__failed_title" = "Impossibile\nspostare"; "lightning__move_to_spark__failed_unknown" = "Qualcosa è andato storto durante la preparazione dello spostamento. Riprova."; "lightning__move_to_spark__fee" = "Commissione di instradamento"; "lightning__move_to_spark__in_flight_text" = "Il pagamento è in arrivo e compare su Spark una volta completato. Poi chiudi la connessione svuotata per restituire la sua riserva ai Risparmi."; "lightning__move_to_spark__intro_text" = "Blocktank sta chiudendo le sue connessioni Lightning. Sposta ora il tuo saldo su Spark: è l'opzione con le commissioni più basse e resta in Spesa."; -"lightning__move_to_spark__intro_title" = "La tua connessione si sta chiudendo"; -"lightning__move_to_spark__move_rest" = "Sposta Il Resto"; +"lightning__move_to_spark__intro_title" = "La tua connessione\nsi sta chiudendo"; +"lightning__move_to_spark__move_half" = "Sposta Metà"; +"lightning__move_to_spark__move_now" = "Sposta Ora"; +"lightning__move_to_spark__move_rest" = "Sposta Resto"; "lightning__move_to_spark__nav_title" = "Sposta Su Spark"; "lightning__move_to_spark__quoting" = "Ricerca di un percorso e della sua commissione…"; "lightning__move_to_spark__swipe" = "Swipe Per Spostare"; "lightning__move_to_spark__too_large_none" = "Chiudi invece la connessione per restituire il saldo ai Risparmi."; "lightning__move_to_spark__too_large_smaller" = "Puoi spostarlo in parti più piccole."; "lightning__move_to_spark__too_large_text" = "{amount} è troppo grande per essere instradato su Spark in un unico pagamento."; -"lightning__move_to_spark__too_large_title" = "Importo troppo grande"; -"lightning__move_to_spark__try" = "Sposta {amount}"; +"lightning__move_to_spark__too_large_title" = "Importo troppo\ngrande"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Commissione di riscatto"; "lightning__spark_confirmations" = "{count}/3 conferme"; diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings index 734d7a6e5..0c0501135 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -344,29 +344,29 @@ "lightning__order_fee" = "Order kosten"; "lightning__balance" = "Saldo"; "lightning__move_to_spark__amount" = "Bedrag"; -"lightning__move_to_spark__close" = "Verbinding sluiten"; "lightning__move_to_spark__confirm_text" = "Uw saldo gaat via Lightning naar Spark, de goedkoopste manier om het op uw bestedingssaldo te houden. De routeringsvergoeding hieronder is het maximum dat u betaalt."; -"lightning__move_to_spark__confirm_title" = "Verplaatsen naar Spark"; +"lightning__move_to_spark__confirm_title" = "Verplaatsen naar\nSpark"; "lightning__move_to_spark__done_rest_text" = "Een deel van uw saldo staat nu op Spark. Verplaats de rest op dezelfde manier en sluit daarna de lege verbinding."; "lightning__move_to_spark__done_text" = "Uw saldo staat nu op Spark. Sluit de lege verbinding om de reserve terug te geven aan uw spaargeld."; -"lightning__move_to_spark__done_title" = "Verplaatst naar Spark"; +"lightning__move_to_spark__done_title" = "Verplaatst naar\nSpark"; "lightning__move_to_spark__failed_nothing" = "Uw Lightning-verbindingen hebben geen saldo meer om te verplaatsen."; "lightning__move_to_spark__failed_spark" = "Spark is nog niet klaar om te ontvangen. Probeer het zo meteen opnieuw."; -"lightning__move_to_spark__failed_title" = "Verplaatsen mislukt"; +"lightning__move_to_spark__failed_title" = "Verplaatsen\nmislukt"; "lightning__move_to_spark__failed_unknown" = "Er is iets misgegaan bij het voorbereiden van de verplaatsing. Probeer het opnieuw."; "lightning__move_to_spark__fee" = "Routeringsvergoeding"; "lightning__move_to_spark__in_flight_text" = "De betaling is onderweg en verschijnt in Spark zodra ze is afgerond. Sluit daarna de lege verbinding om de reserve terug te geven aan uw spaargeld."; "lightning__move_to_spark__intro_text" = "Blocktank sluit zijn Lightning-verbindingen. Verplaats uw saldo nu naar Spark: dit is de goedkoopste optie en het blijft op uw bestedingssaldo."; -"lightning__move_to_spark__intro_title" = "Uw verbinding wordt gesloten"; -"lightning__move_to_spark__move_rest" = "De rest verplaatsen"; +"lightning__move_to_spark__intro_title" = "Uw verbinding wordt\ngesloten"; +"lightning__move_to_spark__move_half" = "Stuur helft"; +"lightning__move_to_spark__move_now" = "Stuur nu"; +"lightning__move_to_spark__move_rest" = "Stuur rest"; "lightning__move_to_spark__nav_title" = "Verplaatsen naar Spark"; "lightning__move_to_spark__quoting" = "Route en vergoeding zoeken…"; "lightning__move_to_spark__swipe" = "Veeg om te verplaatsen"; "lightning__move_to_spark__too_large_none" = "Sluit in plaats daarvan de verbinding om het saldo terug te geven aan uw spaargeld."; "lightning__move_to_spark__too_large_smaller" = "U kunt het in kleinere delen verplaatsen."; "lightning__move_to_spark__too_large_text" = "{amount} is te groot om in één betaling naar Spark te routeren."; -"lightning__move_to_spark__too_large_title" = "Bedrag te groot"; -"lightning__move_to_spark__try" = "{amount} verplaatsen"; +"lightning__move_to_spark__too_large_title" = "Bedrag is\nte groot"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Claimkosten"; "lightning__spark_confirmations" = "{count}/3 bevestigingen"; diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings index 69ddd7a76..36d0453b9 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -346,29 +346,29 @@ "lightning__order_fee" = "Opłata za zamówienie"; "lightning__balance" = "Saldo"; "lightning__move_to_spark__amount" = "Kwota"; -"lightning__move_to_spark__close" = "Zamknij połączenie"; "lightning__move_to_spark__confirm_text" = "Twoje saldo trafia do Spark przez Lightning, najtańszym sposobem na pozostawienie go w Wydatkach. Opłata za routing poniżej to maksimum, jakie zapłacisz."; -"lightning__move_to_spark__confirm_title" = "Przenieś do Spark"; +"lightning__move_to_spark__confirm_title" = "Przenieś do\nSpark"; "lightning__move_to_spark__done_rest_text" = "Część Twojego salda jest teraz w Spark. Przenieś resztę w ten sam sposób, a następnie zamknij opróżnione połączenie."; "lightning__move_to_spark__done_text" = "Twoje saldo jest teraz w Spark. Zamknij opróżnione połączenie, aby zwrócić jego rezerwę do Oszczędności."; -"lightning__move_to_spark__done_title" = "Przeniesiono do Spark"; +"lightning__move_to_spark__done_title" = "Przeniesiono do\nSpark"; "lightning__move_to_spark__failed_nothing" = "Twoje połączenia Lightning nie mają już salda do przeniesienia."; "lightning__move_to_spark__failed_spark" = "Spark nie jest jeszcze gotowy do odbioru. Spróbuj ponownie za chwilę."; -"lightning__move_to_spark__failed_title" = "Nie udało się przenieść"; +"lightning__move_to_spark__failed_title" = "Nie udało się\nprzenieść"; "lightning__move_to_spark__failed_unknown" = "Coś poszło nie tak podczas przygotowania przeniesienia. Spróbuj ponownie."; "lightning__move_to_spark__fee" = "Opłata za routing"; "lightning__move_to_spark__in_flight_text" = "Płatność jest w drodze i pojawi się w Spark po rozliczeniu. Następnie zamknij opróżnione połączenie, aby zwrócić jego rezerwę do Oszczędności."; "lightning__move_to_spark__intro_text" = "Blocktank zamyka swoje połączenia Lightning. Przenieś teraz saldo do Spark: to opcja z najniższymi opłatami, a środki zostają w Wydatkach."; -"lightning__move_to_spark__intro_title" = "Twoje połączenie się zamyka"; -"lightning__move_to_spark__move_rest" = "Przenieś resztę"; +"lightning__move_to_spark__intro_title" = "Twoje połączenie\nsię zamyka"; +"lightning__move_to_spark__move_half" = "Połowę"; +"lightning__move_to_spark__move_now" = "Przenieś"; +"lightning__move_to_spark__move_rest" = "Resztę"; "lightning__move_to_spark__nav_title" = "Przenieś do Spark"; "lightning__move_to_spark__quoting" = "Szukanie trasy i jej opłaty…"; "lightning__move_to_spark__swipe" = "Przesuń, aby przenieść"; "lightning__move_to_spark__too_large_none" = "Zamiast tego zamknij połączenie, aby zwrócić saldo do Oszczędności."; "lightning__move_to_spark__too_large_smaller" = "Możesz przenieść je w mniejszych częściach."; "lightning__move_to_spark__too_large_text" = "Kwota {amount} jest zbyt duża, aby przekazać ją do Spark jedną płatnością."; -"lightning__move_to_spark__too_large_title" = "Kwota zbyt duża"; -"lightning__move_to_spark__try" = "Przenieś {amount}"; +"lightning__move_to_spark__too_large_title" = "Kwota jest\nzbyt duża"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Opłata za odbiór"; "lightning__spark_confirmations" = "{count}/3 potwierdzeń"; diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings index 7673ef186..4d8cbf80e 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -346,29 +346,29 @@ "lightning__order_fee" = "Taxa de pedido"; "lightning__balance" = "Saldo"; "lightning__move_to_spark__amount" = "Valor"; -"lightning__move_to_spark__close" = "Fechar Conexão"; "lightning__move_to_spark__confirm_text" = "Seu saldo vai para o Spark pela Lightning, a forma de menor taxa de mantê-lo em Gastos. A taxa de roteamento abaixo é o máximo que você paga."; -"lightning__move_to_spark__confirm_title" = "Mover para o Spark"; +"lightning__move_to_spark__confirm_title" = "Mover para o\nSpark"; "lightning__move_to_spark__done_rest_text" = "Parte do seu saldo agora está no Spark. Mova o resto da mesma forma e depois feche a conexão esvaziada."; "lightning__move_to_spark__done_text" = "Seu saldo agora está no Spark. Feche a conexão esvaziada para devolver a reserva à Poupança."; -"lightning__move_to_spark__done_title" = "Movido para o Spark"; +"lightning__move_to_spark__done_title" = "Movido para o\nSpark"; "lightning__move_to_spark__failed_nothing" = "Suas conexões Lightning não têm mais saldo para mover."; "lightning__move_to_spark__failed_spark" = "O Spark ainda não está pronto para receber. Tente novamente em instantes."; -"lightning__move_to_spark__failed_title" = "Não foi possível mover"; +"lightning__move_to_spark__failed_title" = "Não foi possível\nmover"; "lightning__move_to_spark__failed_unknown" = "Algo deu errado ao preparar a movimentação. Tente novamente."; "lightning__move_to_spark__fee" = "Taxa de roteamento"; "lightning__move_to_spark__in_flight_text" = "O pagamento está a caminho e aparece no Spark assim que for liquidado. Depois, feche a conexão esvaziada para devolver a reserva à Poupança."; "lightning__move_to_spark__intro_text" = "O Blocktank está fechando suas conexões Lightning. Mova seu saldo para o Spark agora: é a opção de menor taxa e ele continua em Gastos."; -"lightning__move_to_spark__intro_title" = "Sua conexão está fechando"; -"lightning__move_to_spark__move_rest" = "Mover o Resto"; +"lightning__move_to_spark__intro_title" = "Sua conexão está\nfechando"; +"lightning__move_to_spark__move_half" = "Mover Metade"; +"lightning__move_to_spark__move_now" = "Mover Agora"; +"lightning__move_to_spark__move_rest" = "Mover Resto"; "lightning__move_to_spark__nav_title" = "Mover Para o Spark"; "lightning__move_to_spark__quoting" = "Buscando uma rota e sua taxa…"; "lightning__move_to_spark__swipe" = "Deslizar Para Mover"; "lightning__move_to_spark__too_large_none" = "Feche a conexão para devolver o saldo à Poupança."; "lightning__move_to_spark__too_large_smaller" = "Você pode movê-lo em partes menores."; "lightning__move_to_spark__too_large_text" = "{amount} é grande demais para ser roteado ao Spark em um único pagamento."; -"lightning__move_to_spark__too_large_title" = "Valor grande demais"; -"lightning__move_to_spark__try" = "Mover {amount}"; +"lightning__move_to_spark__too_large_title" = "Valor grande\ndemais"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Taxa de resgate"; "lightning__spark_confirmations" = "{count}/3 confirmações"; diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings index 69c618da8..08d2dec28 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -344,29 +344,29 @@ "lightning__order_fee" = "Taxa de pedido"; "lightning__balance" = "Saldo"; "lightning__move_to_spark__amount" = "Montante"; -"lightning__move_to_spark__close" = "Fechar Conexão"; "lightning__move_to_spark__confirm_text" = "O seu saldo passa para o Spark através do Lightning, a forma com as taxas mais baixas de o manter em Gastos. A taxa de encaminhamento abaixo é o máximo que paga."; -"lightning__move_to_spark__confirm_title" = "Mover para o Spark"; +"lightning__move_to_spark__confirm_title" = "Mover para o\nSpark"; "lightning__move_to_spark__done_rest_text" = "Parte do seu saldo está agora no Spark. Mova o resto da mesma forma e depois feche a conexão esvaziada."; "lightning__move_to_spark__done_text" = "O seu saldo está agora no Spark. Feche a conexão esvaziada para devolver a reserva à Poupança."; -"lightning__move_to_spark__done_title" = "Movido para o Spark"; +"lightning__move_to_spark__done_title" = "Movido para o\nSpark"; "lightning__move_to_spark__failed_nothing" = "As suas conexões Lightning já não têm saldo para mover."; "lightning__move_to_spark__failed_spark" = "O Spark ainda não está pronto para receber. Tente novamente dentro de instantes."; -"lightning__move_to_spark__failed_title" = "Não foi possível mover"; +"lightning__move_to_spark__failed_title" = "Não foi possível\nmover"; "lightning__move_to_spark__failed_unknown" = "Algo correu mal ao preparar a movimentação. Tente novamente."; "lightning__move_to_spark__fee" = "Taxa de encaminhamento"; "lightning__move_to_spark__in_flight_text" = "O pagamento está a caminho e aparece no Spark quando for liquidado. Depois, feche a conexão esvaziada para devolver a reserva à Poupança."; "lightning__move_to_spark__intro_text" = "O Blocktank está a fechar as suas conexões Lightning. Mova já o seu saldo para o Spark: é a opção com as taxas mais baixas e fica em Gastos."; -"lightning__move_to_spark__intro_title" = "A sua conexão está a fechar"; -"lightning__move_to_spark__move_rest" = "Mover o Resto"; +"lightning__move_to_spark__intro_title" = "A sua conexão\nvai fechar"; +"lightning__move_to_spark__move_half" = "Mover Metade"; +"lightning__move_to_spark__move_now" = "Mover Agora"; +"lightning__move_to_spark__move_rest" = "Mover Resto"; "lightning__move_to_spark__nav_title" = "Mover Para o Spark"; "lightning__move_to_spark__quoting" = "A procurar uma rota e a respetiva taxa…"; "lightning__move_to_spark__swipe" = "Deslizar Para Mover"; "lightning__move_to_spark__too_large_none" = "Feche antes a conexão para devolver o saldo à Poupança."; "lightning__move_to_spark__too_large_smaller" = "Pode movê-lo em partes mais pequenas."; "lightning__move_to_spark__too_large_text" = "{amount} é demasiado grande para ser encaminhado para o Spark num só pagamento."; -"lightning__move_to_spark__too_large_title" = "Montante demasiado grande"; -"lightning__move_to_spark__try" = "Mover {amount}"; +"lightning__move_to_spark__too_large_title" = "Montante demasiado\ngrande"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Taxa de resgate"; "lightning__spark_confirmations" = "{count}/3 confirmações"; diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings index 665e89f75..108035862 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -341,29 +341,29 @@ "lightning__order_fee" = "Комиссия Ордера"; "lightning__balance" = "Баланс"; "lightning__move_to_spark__amount" = "Сумма"; -"lightning__move_to_spark__close" = "Закрыть Соединение"; "lightning__move_to_spark__confirm_text" = "Ваш баланс переходит в Spark через Lightning — самым дешёвым по комиссиям способом сохранить его в Расходах. Комиссия за маршрутизацию ниже — максимум, который вы платите."; -"lightning__move_to_spark__confirm_title" = "Перенос в Spark"; +"lightning__move_to_spark__confirm_title" = "Перенос в\nSpark"; "lightning__move_to_spark__done_rest_text" = "Часть вашего баланса теперь в Spark. Перенесите остаток таким же способом, затем закройте опустевшее соединение."; "lightning__move_to_spark__done_text" = "Ваш баланс теперь в Spark. Закройте опустевшее соединение, чтобы вернуть его резерв в Сбережения."; -"lightning__move_to_spark__done_title" = "Перенесено в Spark"; +"lightning__move_to_spark__done_title" = "Перенесено в\nSpark"; "lightning__move_to_spark__failed_nothing" = "На ваших соединениях Lightning больше нет баланса для переноса."; "lightning__move_to_spark__failed_spark" = "Spark пока не готов к получению. Попробуйте ещё раз чуть позже."; -"lightning__move_to_spark__failed_title" = "Не удалось перенести"; +"lightning__move_to_spark__failed_title" = "Не удалось\nперенести"; "lightning__move_to_spark__failed_unknown" = "Что-то пошло не так при подготовке переноса. Попробуйте ещё раз."; "lightning__move_to_spark__fee" = "Комиссия за маршрутизацию"; "lightning__move_to_spark__in_flight_text" = "Платёж в пути и появится в Spark после завершения. Затем закройте опустевшее соединение, чтобы вернуть его резерв в Сбережения."; "lightning__move_to_spark__intro_text" = "Blocktank закрывает свои соединения Lightning. Перенесите баланс в Spark сейчас: это вариант с самой низкой комиссией, и средства остаются в Расходах."; -"lightning__move_to_spark__intro_title" = "Ваше соединение закрывается"; -"lightning__move_to_spark__move_rest" = "Перенести Остаток"; +"lightning__move_to_spark__intro_title" = "Ваше соединение\nзакрывается"; +"lightning__move_to_spark__move_half" = "Половину"; +"lightning__move_to_spark__move_now" = "Перенести"; +"lightning__move_to_spark__move_rest" = "Остаток"; "lightning__move_to_spark__nav_title" = "Перенос в Spark"; "lightning__move_to_spark__quoting" = "Поиск маршрута и его комиссии…"; "lightning__move_to_spark__swipe" = "Проведите для Переноса"; "lightning__move_to_spark__too_large_none" = "Вместо этого закройте соединение, чтобы вернуть баланс в Сбережения."; "lightning__move_to_spark__too_large_smaller" = "Вы можете перенести его меньшими частями."; "lightning__move_to_spark__too_large_text" = "{amount} — слишком большая сумма для маршрутизации в Spark одним платежом."; -"lightning__move_to_spark__too_large_title" = "Сумма слишком велика"; -"lightning__move_to_spark__try" = "Перенести {amount}"; +"lightning__move_to_spark__too_large_title" = "Сумма слишком\nвелика"; "lightning__spark" = "Spark"; "lightning__spark_claim_fee" = "Комиссия за зачисление"; "lightning__spark_confirmations" = "{count}/3 подтверждений"; diff --git a/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift b/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift index 87c3d6c81..b2ebbd892 100644 --- a/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift +++ b/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift @@ -88,7 +88,7 @@ struct MoveToSparkView: View { .accessibilityIdentifier("MoveToSparkFeePaid") illustration("check") HStack(spacing: 16) { - CustomButton(title: t("common__ok"), variant: .secondary) { + CustomButton(title: t("common__later"), variant: .secondary) { navigation.navigateBack() } if hasRest { @@ -97,7 +97,7 @@ struct MoveToSparkView: View { } .accessibilityIdentifier("MoveToSparkMoveRest") } else { - CustomButton(title: t("lightning__move_to_spark__close")) { + CustomButton(title: t("common__close")) { // Closing returns to Lightning Connections through the channel's detail, as a close from there does. navigation.navigateBack() navigation.navigate(.connections) @@ -123,7 +123,7 @@ struct MoveToSparkView: View { CustomButton(title: t("common__cancel"), variant: .secondary) { navigation.navigateBack() } - CustomButton(title: t("lightning__move_to_spark__try", variables: ["amount": currency.primaryAmountText(sats: smallerSats)])) { + CustomButton(title: t("lightning__move_to_spark__move_half")) { await manager.quote(wallet: wallet, targetSats: smallerSats) } .accessibilityIdentifier("MoveToSparkTrySmaller") diff --git a/Bitkit/Views/Sheets/MoveToSparkSheet.swift b/Bitkit/Views/Sheets/MoveToSparkSheet.swift index dd775a94b..b29e62db9 100644 --- a/Bitkit/Views/Sheets/MoveToSparkSheet.swift +++ b/Bitkit/Views/Sheets/MoveToSparkSheet.swift @@ -41,7 +41,7 @@ struct MoveToSparkSheet: View { sheets.hideSheet() } .accessibilityIdentifier("MoveToSparkIntroLater") - CustomButton(title: t("lightning__move_to_spark__nav_title"), shouldExpand: true) { + CustomButton(title: t("lightning__move_to_spark__move_now"), shouldExpand: true) { sheets.hideSheet() navigation.navigate(.moveToSpark) } From 03e5d35f65ca43864f63db489e393ab74858b95a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 20:50:17 +0100 Subject: [PATCH 5/9] fix: keep the move to spark sheet copy whole on small screens --- Bitkit/Views/Sheets/MoveToSparkSheet.swift | 27 +++++++++------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/Bitkit/Views/Sheets/MoveToSparkSheet.swift b/Bitkit/Views/Sheets/MoveToSparkSheet.swift index b29e62db9..318efa3a7 100644 --- a/Bitkit/Views/Sheets/MoveToSparkSheet.swift +++ b/Bitkit/Views/Sheets/MoveToSparkSheet.swift @@ -16,23 +16,18 @@ struct MoveToSparkSheet: View { Sheet(id: .moveToSpark, data: config) { VStack(alignment: .leading, spacing: 0) { SheetHeader(title: t("lightning__move_to_spark__nav_title")) - GeometryReader { geometry in - ScrollView(showsIndicators: false) { - VStack(alignment: .leading, spacing: 32) { - Image("lightning") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 256, height: 256) - .frame(maxWidth: .infinity) - VStack(alignment: .leading, spacing: 8) { - DisplayText(t("lightning__move_to_spark__intro_title"), accentColor: .purpleAccent) - BodyMText(t("lightning__move_to_spark__intro_text")) - .fixedSize(horizontal: false, vertical: true) - } - } - .frame(minHeight: geometry.size.height, alignment: .bottom) - } + // The illustration shrinks on small screens so the copy below it stays whole. + Image("lightning") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 256, maxHeight: 256) + .frame(maxWidth: .infinity, maxHeight: .infinity) + VStack(alignment: .leading, spacing: 8) { + DisplayText(t("lightning__move_to_spark__intro_title"), accentColor: .purpleAccent) + BodyMText(t("lightning__move_to_spark__intro_text")) + .fixedSize(horizontal: false, vertical: true) } + .padding(.top, 32) let buttonLayout = dynamicTypeSize.isAccessibilitySize ? AnyLayout(VStackLayout(spacing: 16)) : AnyLayout(HStackLayout(spacing: 16)) From 32f13bc1797b45c4ce5139b9f657959d205b21bf Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 21:09:05 +0100 Subject: [PATCH 6/9] fix: keep move to spark headlines whole on small iphones --- Bitkit/Views/Settings/Advanced/MoveToSparkView.swift | 1 + Bitkit/Views/Sheets/MoveToSparkSheet.swift | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift b/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift index b2ebbd892..db5ac202d 100644 --- a/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift +++ b/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift @@ -37,6 +37,7 @@ struct MoveToSparkView: View { private func header(title: String, text: String) -> some View { VStack(alignment: .leading, spacing: 16) { DisplayText(title, accentColor: .purpleAccent) + .fixedSize(horizontal: false, vertical: true) BodyMText(text) .fixedSize(horizontal: false, vertical: true) } diff --git a/Bitkit/Views/Sheets/MoveToSparkSheet.swift b/Bitkit/Views/Sheets/MoveToSparkSheet.swift index 318efa3a7..5e2d41099 100644 --- a/Bitkit/Views/Sheets/MoveToSparkSheet.swift +++ b/Bitkit/Views/Sheets/MoveToSparkSheet.swift @@ -24,10 +24,12 @@ struct MoveToSparkSheet: View { .frame(maxWidth: .infinity, maxHeight: .infinity) VStack(alignment: .leading, spacing: 8) { DisplayText(t("lightning__move_to_spark__intro_title"), accentColor: .purpleAccent) + .fixedSize(horizontal: false, vertical: true) BodyMText(t("lightning__move_to_spark__intro_text")) .fixedSize(horizontal: false, vertical: true) } .padding(.top, 32) + .layoutPriority(1) let buttonLayout = dynamicTypeSize.isAccessibilitySize ? AnyLayout(VStackLayout(spacing: 16)) : AnyLayout(HStackLayout(spacing: 16)) From fa535a9ddc5c94541845786e7ed9fd07f3ef824e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 5 Nov 2026 03:14:32 +0100 Subject: [PATCH 7/9] fix: build move to spark without the spark exit --- Bitkit/Managers/MoveToSparkManager.swift | 2 +- .../TimedSheets/MoveToSparkTimedSheet.swift | 2 +- Bitkit/Views/HomeScreen.swift | 2 +- .../Advanced/LightningConnectionsView.swift | 2 +- .../Settings/Advanced/MoveToSparkView.swift | 26 ++++++++++++++++--- .../Views/Wallets/SpendingWalletScreen.swift | 2 +- 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Bitkit/Managers/MoveToSparkManager.swift b/Bitkit/Managers/MoveToSparkManager.swift index 980d6ee42..f0a211e47 100644 --- a/Bitkit/Managers/MoveToSparkManager.swift +++ b/Bitkit/Managers/MoveToSparkManager.swift @@ -157,7 +157,7 @@ final class MoveToSparkManager { let limitSats = MoveToSparkMath.movableSats(channels: channels) let wantedSats = min(targetSats ?? limitSats, limitSats) guard wantedSats >= MoveToSparkMath.minMoveSats else { throw MoveToSparkError.nothingToMove } - guard spark.isReady, !spark.isExiting else { throw MoveToSparkError.sparkUnavailable } + guard spark.isReady else { throw MoveToSparkError.sparkUnavailable } let invoice = try await spark.receive( amountSats: nil, diff --git a/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift b/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift index 826077406..a32c0c625 100644 --- a/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift +++ b/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift @@ -12,7 +12,7 @@ struct MoveToSparkTimedSheet: TimedSheetItem { func shouldShow() async -> Bool { guard !app.hasSeenMoveToSparkIntro else { return false } let spark = SparkManager.shared - return MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: spark.isReady && !spark.isExiting) + return MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: spark.isReady) } func onShown() { diff --git a/Bitkit/Views/HomeScreen.swift b/Bitkit/Views/HomeScreen.swift index fb7e2b1dc..e71f51710 100644 --- a/Bitkit/Views/HomeScreen.swift +++ b/Bitkit/Views/HomeScreen.swift @@ -10,7 +10,7 @@ struct HomeScreen: View { @EnvironmentObject var wallet: WalletViewModel private var canMoveToSpark: Bool { - MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: SparkManager.shared.isReady && !SparkManager.shared.isExiting) + MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: SparkManager.shared.isReady) } @State private var scrollPosition: Int? = 0 diff --git a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift index cbef45199..6b14fb842 100644 --- a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift +++ b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift @@ -157,7 +157,7 @@ struct LightningConnectionsView: View { if MoveToSparkMath.canMove( channels: wallet.channels, - isSparkUsable: SparkManager.shared.isReady && !SparkManager.shared.isExiting + isSparkUsable: SparkManager.shared.isReady ) { CustomButton(title: t("lightning__move_to_spark__nav_title")) { navigation.navigate(.moveToSpark) diff --git a/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift b/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift index db5ac202d..b043afb4d 100644 --- a/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift +++ b/Bitkit/Views/Settings/Advanced/MoveToSparkView.swift @@ -64,9 +64,9 @@ struct MoveToSparkView: View { VStack(alignment: .leading, spacing: 0) { header(title: t("lightning__move_to_spark__confirm_title"), text: t("lightning__move_to_spark__confirm_text")) .padding(.bottom, 32) - SparkExitAmountRow(label: t("lightning__move_to_spark__amount"), sats: quote.amountSats) + MoveToSparkAmountRow(label: t("lightning__move_to_spark__amount"), sats: quote.amountSats) .accessibilityIdentifier("MoveToSparkAmount") - SparkExitAmountRow(label: t("lightning__move_to_spark__fee"), sats: quote.feeSats) + MoveToSparkAmountRow(label: t("lightning__move_to_spark__fee"), sats: quote.feeSats) .accessibilityIdentifier("MoveToSparkFee") illustration("lightning") SwipeButton(title: t("lightning__move_to_spark__swipe"), accentColor: .purpleAccent, isLoading: isMoving) { @@ -83,9 +83,9 @@ struct MoveToSparkView: View { text: hasRest ? t("lightning__move_to_spark__done_rest_text") : t("lightning__move_to_spark__done_text") ) .padding(.bottom, 32) - SparkExitAmountRow(label: t("lightning__move_to_spark__amount"), sats: amountSats) + MoveToSparkAmountRow(label: t("lightning__move_to_spark__amount"), sats: amountSats) .accessibilityIdentifier("MoveToSparkMoved") - SparkExitAmountRow(label: t("lightning__move_to_spark__fee"), sats: feeSats) + MoveToSparkAmountRow(label: t("lightning__move_to_spark__fee"), sats: feeSats) .accessibilityIdentifier("MoveToSparkFeePaid") illustration("check") HStack(spacing: 16) { @@ -164,3 +164,21 @@ struct MoveToSparkView: View { } } } + +/// A labelled amount, as the Move to Spark screens list the amount and its routing fee. +struct MoveToSparkAmountRow: View { + let label: String + let sats: UInt64 + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .center, spacing: 0) { + CaptionBText(label, textColor: .textPrimary) + Spacer() + MoneyText(sats: Int(sats), size: .captionB, symbol: true) + } + .frame(height: 50) + CustomDivider() + } + } +} diff --git a/Bitkit/Views/Wallets/SpendingWalletScreen.swift b/Bitkit/Views/Wallets/SpendingWalletScreen.swift index 7a5381403..3f684a303 100644 --- a/Bitkit/Views/Wallets/SpendingWalletScreen.swift +++ b/Bitkit/Views/Wallets/SpendingWalletScreen.swift @@ -8,7 +8,7 @@ struct SpendingWalletScreen: View { @EnvironmentObject var wallet: WalletViewModel private var canMoveToSpark: Bool { - MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: SparkManager.shared.isReady && !SparkManager.shared.isExiting) + MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: SparkManager.shared.isReady) } private var shouldShowOnboarding: Bool { From 35fdd91bbf52a3a133d78e7768ce9fc6f0698527 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 5 Nov 2026 14:18:51 +0100 Subject: [PATCH 8/9] chore: drop the fee from the move changelog line --- changelog.d/next/1206.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/next/1206.added.md b/changelog.d/next/1206.added.md index 4562661bb..80633ba9b 100644 --- a/changelog.d/next/1206.added.md +++ b/changelog.d/next/1206.added.md @@ -1 +1 @@ -Move to Spark: move a Lightning connection's balance to Spark at the lowest fee, then close the emptied connection to return its reserve to Savings. +Move to Spark: move a Lightning connection's balance to Spark, then close the emptied connection to return its reserve to Savings. From b6612b779fc00d5c2d127c82c359deaafe350bb9 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 5 Nov 2026 15:16:11 +0100 Subject: [PATCH 9/9] refactor: gate move to spark on one spark check --- Bitkit/Managers/MoveToSparkManager.swift | 8 +++++++- Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift | 2 +- Bitkit/Views/HomeScreen.swift | 2 +- .../Settings/Advanced/LightningConnectionsView.swift | 2 +- Bitkit/Views/Wallets/SpendingWalletScreen.swift | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Bitkit/Managers/MoveToSparkManager.swift b/Bitkit/Managers/MoveToSparkManager.swift index f0a211e47..3f2a52e40 100644 --- a/Bitkit/Managers/MoveToSparkManager.swift +++ b/Bitkit/Managers/MoveToSparkManager.swift @@ -16,6 +16,12 @@ enum MoveToSparkMath { (channels ?? []).filter(\.isUsable).map { $0.nextOutboundHtlcLimitMsat / 1000 }.reduce(0, +) } + /// Whether Spark can take a move; every Move to Spark check goes through here. + @MainActor + static func isSparkUsable(_ spark: SparkManager) -> Bool { + spark.isReady + } + /// Whether the channels hold a balance worth moving to a usable Spark wallet. static func canMove(channels: [ChannelDetails]?, isSparkUsable: Bool) -> Bool { isSparkUsable && movableSats(channels: channels) >= minMoveSats @@ -157,7 +163,7 @@ final class MoveToSparkManager { let limitSats = MoveToSparkMath.movableSats(channels: channels) let wantedSats = min(targetSats ?? limitSats, limitSats) guard wantedSats >= MoveToSparkMath.minMoveSats else { throw MoveToSparkError.nothingToMove } - guard spark.isReady else { throw MoveToSparkError.sparkUnavailable } + guard MoveToSparkMath.isSparkUsable(spark) else { throw MoveToSparkError.sparkUnavailable } let invoice = try await spark.receive( amountSats: nil, diff --git a/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift b/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift index a32c0c625..cf173e027 100644 --- a/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift +++ b/Bitkit/Managers/TimedSheets/MoveToSparkTimedSheet.swift @@ -12,7 +12,7 @@ struct MoveToSparkTimedSheet: TimedSheetItem { func shouldShow() async -> Bool { guard !app.hasSeenMoveToSparkIntro else { return false } let spark = SparkManager.shared - return MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: spark.isReady) + return MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: MoveToSparkMath.isSparkUsable(spark)) } func onShown() { diff --git a/Bitkit/Views/HomeScreen.swift b/Bitkit/Views/HomeScreen.swift index e71f51710..6830e236e 100644 --- a/Bitkit/Views/HomeScreen.swift +++ b/Bitkit/Views/HomeScreen.swift @@ -10,7 +10,7 @@ struct HomeScreen: View { @EnvironmentObject var wallet: WalletViewModel private var canMoveToSpark: Bool { - MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: SparkManager.shared.isReady) + MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: MoveToSparkMath.isSparkUsable(SparkManager.shared)) } @State private var scrollPosition: Int? = 0 diff --git a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift index 6b14fb842..bd48beb19 100644 --- a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift +++ b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift @@ -157,7 +157,7 @@ struct LightningConnectionsView: View { if MoveToSparkMath.canMove( channels: wallet.channels, - isSparkUsable: SparkManager.shared.isReady + isSparkUsable: MoveToSparkMath.isSparkUsable(SparkManager.shared) ) { CustomButton(title: t("lightning__move_to_spark__nav_title")) { navigation.navigate(.moveToSpark) diff --git a/Bitkit/Views/Wallets/SpendingWalletScreen.swift b/Bitkit/Views/Wallets/SpendingWalletScreen.swift index 3f684a303..f41fd0f6d 100644 --- a/Bitkit/Views/Wallets/SpendingWalletScreen.swift +++ b/Bitkit/Views/Wallets/SpendingWalletScreen.swift @@ -8,7 +8,7 @@ struct SpendingWalletScreen: View { @EnvironmentObject var wallet: WalletViewModel private var canMoveToSpark: Bool { - MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: SparkManager.shared.isReady) + MoveToSparkMath.canMove(channels: wallet.channels, isSparkUsable: MoveToSparkMath.isSparkUsable(SparkManager.shared)) } private var shouldShowOnboarding: Bool {