Skip to content

Commit bb09eeb

Browse files
committed
feat: show spark in lightning connections
1 parent be90cad commit bb09eeb

20 files changed

Lines changed: 454 additions & 7 deletions

File tree

‎Bitkit/MainNavView.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,7 @@ struct MainNavView: View {
750750
case .addressTypePreference: AddressTypePreferenceView()
751751
case .connections: LightningConnectionsView()
752752
case let .connectionDetail(channelId): LightningConnectionDetailView(channelId: channelId)
753+
case .sparkConnectionDetail: SparkConnectionDetailView()
753754
case let .closeConnection(channel: channel): CloseConnectionConfirmation(channel: channel)
754755
case .node: NodeStateView()
755756
case .electrumSettings: ElectrumSettingsScreen()

‎Bitkit/Managers/SparkManager.swift‎

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ final class SparkManager: ChannelFreeLightningRail {
2323
/// Deposits seen on-chain that Spark claims into `balanceSats` after 3 confirmations.
2424
private(set) var pendingDepositSats: UInt64 = 0
2525
private(set) var recoverableFundsSats: UInt64 = 0
26+
/// On-chain deposits into Spark, pending ones first, for the Lightning Connections screens.
27+
private(set) var deposits: [SparkDeposit] = []
2628
/// Called when Spark claims on-chain deposits, which completes a Savings to Spending transfer.
2729
var onDepositsClaimed: (() -> Void)?
2830
/// Called after the Spark balance or readiness was read again, so balances and the receive invoice can follow.
@@ -38,6 +40,12 @@ final class SparkManager: ChannelFreeLightningRail {
3840
Self.sparkNetwork != nil
3941
}
4042

43+
/// Spark's regtest has no Lightning network, so there a Lightning invoice from another Spark wallet is paid as a
44+
/// Spark transfer, which lets two dev builds pay each other's invoices.
45+
nonisolated static var prefersSpark: Bool {
46+
Env.network == .regtest
47+
}
48+
4149
private static var sparkNetwork: BreezSdkSpark.Network? {
4250
switch Env.network {
4351
case .bitcoin: Env.breezApiKey == nil ? nil : .mainnet
@@ -106,6 +114,7 @@ final class SparkManager: ChannelFreeLightningRail {
106114
balanceSats = 0
107115
pendingDepositSats = 0
108116
recoverableFundsSats = 0
117+
deposits = []
109118
try? FileManager.default.removeItem(at: Env.sparkStorage(walletIndex: walletIndex))
110119
}
111120

@@ -129,15 +138,18 @@ final class SparkManager: ChannelFreeLightningRail {
129138
}
130139
let response = try await requireSdk().sendPayment(request: SendPaymentRequest(
131140
prepareResponse: prepared,
132-
options: .bolt11Invoice(preferSpark: false, completionTimeoutSecs: Self.sendCompletionTimeoutSecs)
141+
options: .bolt11Invoice(preferSpark: Self.prefersSpark, completionTimeoutSecs: Self.sendCompletionTimeoutSecs)
133142
))
134143
Logger.info("Sent Spark payment '\(response.payment.id)' with status '\(response.payment.status)'", context: "SparkManager")
135144
return invoiceDetails.paymentHash
136145
}
137146

138147
nonisolated func estimateRoutingFees(bolt11: String, amountSats: UInt64?) async throws -> UInt64 {
139148
let prepared = try await prepareSend(paymentRequest: bolt11, amountSats: amountSats)
140-
guard case let .bolt11Invoice(_, _, lightningFeeSats) = prepared.paymentMethod else { return 0 }
149+
guard case let .bolt11Invoice(_, sparkTransferFeeSats, lightningFeeSats) = prepared.paymentMethod else { return 0 }
150+
if Self.prefersSpark, let sparkTransferFeeSats {
151+
return sparkTransferFeeSats
152+
}
141153
return lightningFeeSats
142154
}
143155

@@ -187,21 +199,69 @@ final class SparkManager: ChannelFreeLightningRail {
187199
return sdk
188200
}
189201

202+
/// Reads the Spark balance and deposits again, for pull to refresh.
203+
func refreshState() async {
204+
guard sdk != nil else { return }
205+
do {
206+
try await refresh()
207+
} catch {
208+
Logger.warn("Failed to refresh Spark state: \(error)", context: "SparkManager")
209+
}
210+
}
211+
190212
private func refresh() async throws {
191213
let sdk = try await requireSdk()
192214
let info = try await sdk.getInfo(request: GetInfoRequest(ensureSynced: false))
193-
let deposits = try await sdk.listUnclaimedDeposits(request: ListUnclaimedDepositsRequest())
194-
let pending = deposits.deposits.reduce(0) { $0 + $1.amountSats }
215+
let unclaimed = try await sdk.listUnclaimedDeposits(request: ListUnclaimedDepositsRequest()).deposits
216+
let claimed = try await sdk.listPayments(request: ListPaymentsRequest()).payments
217+
.filter { $0.method == .deposit && $0.status == .completed }
218+
let pending = unclaimed.reduce(0) { $0 + $1.amountSats }
195219
let changed = !isReady || balanceSats != info.balanceSats || pendingDepositSats != pending
196220
isReady = true
197221
balanceSats = info.balanceSats
198222
pendingDepositSats = pending
199223
recoverableFundsSats = info.recoverableFundsSats
224+
deposits = await Self.deposits(unclaimed: unclaimed, claimed: claimed)
200225
if changed {
201226
onStateChanged?()
202227
}
203228
}
204229

230+
private static func deposits(unclaimed: [DepositInfo], claimed: [Payment]) async -> [SparkDeposit] {
231+
let confirmations = await onchainConfirmations()
232+
let pending = unclaimed.map {
233+
SparkDeposit(txid: $0.txid, amountSats: $0.amountSats, isClaimed: false, confirmations: confirmations[$0.txid] ?? nil)
234+
}
235+
let done: [SparkDeposit] = claimed.compactMap { payment in
236+
guard case let .deposit(txId, _) = payment.details else { return nil }
237+
return SparkDeposit(
238+
txid: txId,
239+
amountSats: sats(payment.amount),
240+
isClaimed: true,
241+
feeSats: sats(payment.fees),
242+
claimedAt: payment.timestamp
243+
)
244+
}
245+
return pending + done
246+
}
247+
248+
/// Confirmations of the on-chain transactions this wallet sent or received, 0 while unconfirmed.
249+
private static func onchainConfirmations() async -> [String: UInt32?] {
250+
let tip = LightningService.shared.status?.currentBestBlock.height
251+
let payments = await LightningService.shared.listPayments() ?? []
252+
var result: [String: UInt32?] = [:]
253+
for payment in payments {
254+
guard case let .onchain(txid, status) = payment.kind else { continue }
255+
switch status {
256+
case .unconfirmed:
257+
result[txid] = 0
258+
case let .confirmed(_, height, _):
259+
result[txid] = tip.map { $0 >= height ? $0 - height + 1 : 0 }
260+
}
261+
}
262+
return result
263+
}
264+
205265
private func syncActivities() async throws {
206266
let payments = try await requireSdk().listPayments(request: ListPaymentsRequest()).payments
207267
for payment in payments {
@@ -293,6 +353,21 @@ final class SparkManager: ChannelFreeLightningRail {
293353
}
294354
}
295355

356+
/// An on-chain deposit into Spark: pending until Spark claims it after 3 confirmations.
357+
struct SparkDeposit: Identifiable, Equatable {
358+
let txid: String
359+
let amountSats: UInt64
360+
let isClaimed: Bool
361+
var feeSats: UInt64 = 0
362+
var claimedAt: UInt64?
363+
/// On-chain confirmations, known for deposits this wallet sent.
364+
var confirmations: UInt32?
365+
366+
var id: String {
367+
"\(txid)-\(isClaimed)"
368+
}
369+
}
370+
296371
private final class SparkEventListener: BreezSdkSpark.EventListener, @unchecked Sendable {
297372
private let handler: @Sendable (SdkEvent) async -> Void
298373

‎Bitkit/Resources/Localization/ar.lproj/Localizable.strings‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,15 @@
305305
"lightning__transaction" = "المعاملة";
306306
"lightning__order_fee" = "رسوم الطلب";
307307
"lightning__balance" = "الرصيد";
308+
"lightning__spark" = "Spark";
309+
"lightning__spark_claim_fee" = "رسوم المطالبة";
310+
"lightning__spark_confirmations" = "{count}/3 تأكيدات";
311+
"lightning__spark_deposit" = "إيداع";
312+
"lightning__spark_deposits_claimed" = "إيداعات تمت المطالبة بها";
313+
"lightning__spark_deposits_pending" = "إيداعات قيد التنفيذ";
314+
"lightning__spark_provider" = "المزوّد";
315+
"lightning__spark_provider_value" = "Spark عبر Breez";
316+
"lightning__spark_trust" = "يُحفظ هذا الرصيد على Spark الذي تديره Lightspark وBreez وFlashnet. يبقى آمنًا ما دام أحدهم على الأقل يتصرف بأمانة. يمكنهم إيقاف الوصول مؤقتًا، والخروج من دونهم يتطلب خروجًا على السلسلة.";
308317
"lightning__reserve_balance" = "رصيد الاحتياطي (غير قابل للإنفاق)";
309318
"lightning__total_size" = "الحجم الإجمالي للقناة";
310319
"lightning__fees" = "الرسوم";

‎Bitkit/Resources/Localization/ca.lproj/Localizable.strings‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,15 @@
342342
"lightning__transaction" = "Transacció";
343343
"lightning__order_fee" = "Comissió de comanda";
344344
"lightning__balance" = "Saldo";
345+
"lightning__spark" = "Spark";
346+
"lightning__spark_claim_fee" = "Comissió de reclamació";
347+
"lightning__spark_confirmations" = "{count}/3 confirmacions";
348+
"lightning__spark_deposit" = "Dipòsit";
349+
"lightning__spark_deposits_claimed" = "Dipòsits reclamats";
350+
"lightning__spark_deposits_pending" = "Dipòsits en curs";
351+
"lightning__spark_provider" = "Proveïdor";
352+
"lightning__spark_provider_value" = "Spark mitjançant Breez";
353+
"lightning__spark_trust" = "Aquest saldo es guarda a Spark, gestionat per Lightspark, Breez i Flashnet. Està segur mentre almenys un d’ells actuï amb honestedat. Poden pausar l’accés, i sortir-ne sense ells requereix una sortida on-chain.";
345354
"lightning__reserve_balance" = "Saldo de reserva (no gastable)";
346355
"lightning__total_size" = "Mida total del canal";
347356
"lightning__fees" = "Comissions";

‎Bitkit/Resources/Localization/cs.lproj/Localizable.strings‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,15 @@
358358
"lightning__transaction" = "Transakce";
359359
"lightning__order_fee" = "Poplatek za objednávku";
360360
"lightning__balance" = "Zůstatek";
361+
"lightning__spark" = "Spark";
362+
"lightning__spark_claim_fee" = "Poplatek za připsání";
363+
"lightning__spark_confirmations" = "{count}/3 potvrzení";
364+
"lightning__spark_deposit" = "Vklad";
365+
"lightning__spark_deposits_claimed" = "Připsané vklady";
366+
"lightning__spark_deposits_pending" = "Probíhající vklady";
367+
"lightning__spark_provider" = "Poskytovatel";
368+
"lightning__spark_provider_value" = "Spark přes Breez";
369+
"lightning__spark_trust" = "Tento zůstatek je uložen na Spark, který provozují Lightspark, Breez a Flashnet. Je v bezpečí, dokud alespoň jeden z nich jedná poctivě. Mohou pozastavit přístup a odchod bez nich vyžaduje on-chain výstup.";
361370
"lightning__reserve_balance" = "Rezervní zůstatek";
362371
"lightning__total_size" = "Celková velikost kanálu";
363372
"lightning__fees" = "Poplatky";

‎Bitkit/Resources/Localization/de.lproj/Localizable.strings‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,15 @@
356356
"lightning__transaction" = "Transaktion";
357357
"lightning__order_fee" = "Auftragsgebühr";
358358
"lightning__balance" = "Guthaben";
359+
"lightning__spark" = "Spark";
360+
"lightning__spark_claim_fee" = "Einlösegebühr";
361+
"lightning__spark_confirmations" = "{count}/3 Bestätigungen";
362+
"lightning__spark_deposit" = "Einzahlung";
363+
"lightning__spark_deposits_claimed" = "Eingelöste Einzahlungen";
364+
"lightning__spark_deposits_pending" = "Laufende Einzahlungen";
365+
"lightning__spark_provider" = "Anbieter";
366+
"lightning__spark_provider_value" = "Spark über Breez";
367+
"lightning__spark_trust" = "Dieses Guthaben liegt auf Spark, betrieben von Lightspark, Breez und Flashnet. Es ist sicher, solange mindestens einer von ihnen ehrlich handelt. Sie können den Zugang sperren, und ein Ausstieg ohne sie erfordert einen On-Chain-Exit.";
359368
"lightning__reserve_balance" = "Reserveguthaben";
360369
"lightning__total_size" = "Gesamtgröße des Channels";
361370
"lightning__fees" = "Gebühren";

‎Bitkit/Resources/Localization/el.lproj/Localizable.strings‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,15 @@
329329
"lightning__transaction" = "Συναλλαγή";
330330
"lightning__order_fee" = "Τέλος παραγγελίας";
331331
"lightning__balance" = "Υπόλοιπο";
332+
"lightning__spark" = "Spark";
333+
"lightning__spark_claim_fee" = "Χρέωση εξαργύρωσης";
334+
"lightning__spark_confirmations" = "{count}/3 επιβεβαιώσεις";
335+
"lightning__spark_deposit" = "Κατάθεση";
336+
"lightning__spark_deposits_claimed" = "Εξαργυρωμένες καταθέσεις";
337+
"lightning__spark_deposits_pending" = "Καταθέσεις σε εξέλιξη";
338+
"lightning__spark_provider" = "Πάροχος";
339+
"lightning__spark_provider_value" = "Spark μέσω Breez";
340+
"lightning__spark_trust" = "Αυτό το υπόλοιπο φυλάσσεται στο Spark, που λειτουργούν οι Lightspark, Breez και Flashnet. Είναι ασφαλές όσο τουλάχιστον ένας από αυτούς ενεργεί έντιμα. Μπορούν να αναστείλουν την πρόσβαση, και η έξοδος χωρίς αυτούς απαιτεί έξοδο on-chain.";
332341
"lightning__reserve_balance" = "Υπόλοιπο αποθεματικού (μη δαπανήσιμο)";
333342
"lightning__total_size" = "Συνολικό μέγεθος καναλιού";
334343
"lightning__fees" = "Τέλη";

‎Bitkit/Resources/Localization/en.lproj/Localizable.strings‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,15 @@
351351
"lightning__transaction" = "Transaction";
352352
"lightning__order_fee" = "Order fee";
353353
"lightning__balance" = "Balance";
354+
"lightning__spark" = "Spark";
355+
"lightning__spark_claim_fee" = "Claim fee";
356+
"lightning__spark_confirmations" = "{count}/3 confirmations";
357+
"lightning__spark_deposit" = "Deposit";
358+
"lightning__spark_deposits_claimed" = "Claimed deposits";
359+
"lightning__spark_deposits_pending" = "Deposits in progress";
360+
"lightning__spark_provider" = "Provider";
361+
"lightning__spark_provider_value" = "Spark via Breez";
362+
"lightning__spark_trust" = "This balance is held on Spark, run by Lightspark, Breez and Flashnet. It is safe while at least one of them acts honestly. They can pause access, and leaving without them takes an on-chain exit.";
354363
"lightning__reserve_balance" = "Reserve balance (not spendable)";
355364
"lightning__total_size" = "Total channel size";
356365
"lightning__fees" = "Fees";

‎Bitkit/Resources/Localization/es-419.lproj/Localizable.strings‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,15 @@
359359
"lightning__transaction" = "Transacción";
360360
"lightning__order_fee" = "Tarifa de pedido";
361361
"lightning__balance" = "Saldo";
362+
"lightning__spark" = "Spark";
363+
"lightning__spark_claim_fee" = "Comisión de reclamo";
364+
"lightning__spark_confirmations" = "{count}/3 confirmaciones";
365+
"lightning__spark_deposit" = "Depósito";
366+
"lightning__spark_deposits_claimed" = "Depósitos reclamados";
367+
"lightning__spark_deposits_pending" = "Depósitos en curso";
368+
"lightning__spark_provider" = "Proveedor";
369+
"lightning__spark_provider_value" = "Spark mediante Breez";
370+
"lightning__spark_trust" = "Este saldo se guarda en Spark, operado por Lightspark, Breez y Flashnet. Está seguro mientras al menos uno de ellos actúe con honestidad. Pueden pausar el acceso, y salir sin ellos requiere una salida on-chain.";
362371
"lightning__reserve_balance" = "Saldo de reserva (no gastable)";
363372
"lightning__total_size" = "Tamaño total del canal";
364373
"lightning__fees" = "Tarifa";

‎Bitkit/Resources/Localization/es.lproj/Localizable.strings‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,15 @@
343343
"lightning__transaction" = "Transacción";
344344
"lightning__order_fee" = "Tarifa de pedido";
345345
"lightning__balance" = "Saldo";
346+
"lightning__spark" = "Spark";
347+
"lightning__spark_claim_fee" = "Comisión de reclamo";
348+
"lightning__spark_confirmations" = "{count}/3 confirmaciones";
349+
"lightning__spark_deposit" = "Depósito";
350+
"lightning__spark_deposits_claimed" = "Depósitos reclamados";
351+
"lightning__spark_deposits_pending" = "Depósitos en curso";
352+
"lightning__spark_provider" = "Proveedor";
353+
"lightning__spark_provider_value" = "Spark mediante Breez";
354+
"lightning__spark_trust" = "Este saldo se guarda en Spark, operado por Lightspark, Breez y Flashnet. Está seguro mientras al menos uno de ellos actúe con honestidad. Pueden pausar el acceso, y salir sin ellos requiere una salida on-chain.";
346355
"lightning__reserve_balance" = "Saldo de reserva";
347356
"lightning__total_size" = "Tamaño total del canal";
348357
"lightning__fees" = "Comisiones";

0 commit comments

Comments
 (0)