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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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

171 changes: 136 additions & 35 deletions Bitkit/Services/CoreService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,106 @@
}
}
await MainActor.run {
self.cachedTxIdsInBoostTxIds = txIds

Check warning on line 57 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

reference to captured var 'txIds' in concurrently-executing code; this is an error in the Swift 6 language mode
}
} catch {
Logger.error("Failed to refresh boostTxIds cache: \(error)", context: "ActivityService")
}
}

private func mapToCoreTransactionDetails(txid: String, _ details: LDKNode.TransactionDetails) -> BitkitCore.TransactionDetails {
let inputs = details.inputs.map { input in
BitkitCore.TxInput(
txid: input.txid,
vout: input.vout,
scriptsig: input.scriptsig,
witness: input.witness,
sequence: input.sequence
)
}

let outputs = details.outputs.map { output in
BitkitCore.TxOutput(
scriptpubkey: output.scriptpubkey,
scriptpubkeyType: output.scriptpubkeyType,
scriptpubkeyAddress: output.scriptpubkeyAddress,
value: output.value,
n: output.n
)
}

return BitkitCore.TransactionDetails(
txId: txid,
amountSats: details.amountSats,
inputs: inputs,
outputs: outputs
)
}

private func fetchTransactionDetails(txid: String) async -> BitkitCore.TransactionDetails? {
do {
return try await getTransactionDetails(txid: txid)
} catch {
Logger.warn("Failed to fetch stored transaction details for \(txid): \(error)", context: "ActivityService")
return nil
}
}

func getTransactionDetails(txid: String) async throws -> BitkitCore.TransactionDetails? {
try await ServiceQueue.background(.core) {
try BitkitCore.getTransactionDetails(txId: txid)
}
}

// MARK: - Seen Tracking

func isActivitySeen(id: String) async -> Bool {
do {
if let activity = try await getActivityById(activityId: id) {

Check warning on line 112 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

no 'async' operations occur within 'await' expression
switch activity {
case let .onchain(onchain):
return onchain.seenAt != nil
case let .lightning(lightning):
return lightning.seenAt != nil
}
}
} catch {
Logger.error("Failed to check seen status for activity \(id): \(error)", context: "ActivityService")
}
return false
}

func isOnchainActivitySeen(txid: String) async -> Bool {
if let activity = try? await getOnchainActivityByTxId(txid: txid) {
return activity.seenAt != nil
}
return false
}

func markActivityAsSeen(id: String, seenAt: UInt64? = nil) async {
let timestamp = seenAt ?? UInt64(Date().timeIntervalSince1970)

do {
try await ServiceQueue.background(.core) {
try BitkitCore.markActivityAsSeen(activityId: id, seenAt: timestamp)
self.activitiesChangedSubject.send()
}
} catch {
Logger.error("Failed to mark activity \(id) as seen: \(error)", context: "ActivityService")
}
}

func markOnchainActivityAsSeen(txid: String, seenAt: UInt64? = nil) async {
do {
guard let activity = try await getOnchainActivityByTxId(txid: txid) else {
return
}
await markActivityAsSeen(id: activity.id, seenAt: seenAt)
} catch {
Logger.error("Failed to mark onchain activity for \(txid) as seen: \(error)", context: "ActivityService")
}
}

// MARK: - Transaction Status Checks

func wasTransactionReplaced(txid: String) async -> Bool {
Expand All @@ -84,17 +177,15 @@
return false
}

do {
// Check if this transaction's activity has boostTxIds (meaning it replaced other transactions)
// If any of the replaced transactions have the same value, don't show the sheet
guard let onchain = try? await getOnchainActivityByTxId(txid: txid),
!onchain.boostTxIds.isEmpty
else {
return true
}
let onchainActivity = try? await getOnchainActivityByTxId(txid: txid)

if let onchainActivity, onchainActivity.seenAt != nil {
return false
}

// This transaction replaced others - check if any have the same value
for replacedTxid in onchain.boostTxIds {
// If this is a replacement transaction with same value as original, skip the sheet
if let boostTxIds = onchainActivity?.boostTxIds, !boostTxIds.isEmpty {
for replacedTxid in boostTxIds {
if let replaced = try? await getOnchainActivityByTxId(txid: replacedTxid),
replaced.value == value
{
Expand All @@ -105,8 +196,6 @@
return false
}
}
} catch {
Logger.error("Failed to check existing activities for replacement: \(error)", context: "CoreService.shouldShowReceivedSheet")
}

return true
Expand Down Expand Up @@ -213,7 +302,7 @@

private func processOnchainPayment(
_ payment: PaymentDetails,
transactionDetails: TransactionDetails? = nil
transactionDetails: BitkitCore.TransactionDetails? = nil
) async throws {
guard case let .onchain(txid, _) = payment.kind else { return }

Expand Down Expand Up @@ -258,6 +347,7 @@
let feeRate = existingOnchain?.feeRate ?? 1
let preservedAddress = existingOnchain?.address ?? "Loading..."
let doesExist = existingOnchain?.doesExist ?? true
let seenAt = existingOnchain?.seenAt

// Check if this transaction is a channel transfer
if channelId == nil || !isTransfer {
Expand Down Expand Up @@ -309,7 +399,8 @@
channelId: channelId,
transferTxId: transferTxId,
createdAt: UInt64(payment.creationTime.timeIntervalSince1970),
updatedAt: paymentTimestamp
updatedAt: paymentTimestamp,
seenAt: seenAt
)

if existingActivity != nil {
Expand All @@ -321,7 +412,7 @@

// MARK: - Onchain Event Handlers

private func processOnchainTransaction(txid: String, details: TransactionDetails, context: String) async throws {
private func processOnchainTransaction(txid: String, details: BitkitCore.TransactionDetails, context: String) async throws {
guard let payments = LightningService.shared.payments else {
Logger.warn("No payments available for transaction \(txid)", context: context)
return
Expand All @@ -340,15 +431,21 @@
try await processOnchainPayment(payment, transactionDetails: details)
}

func handleOnchainTransactionReceived(txid: String, details: TransactionDetails) async throws {
func handleOnchainTransactionReceived(txid: String, details: LDKNode.TransactionDetails) async throws {
let coreDetails = mapToCoreTransactionDetails(txid: txid, details)

try await ServiceQueue.background(.core) {
try await self.processOnchainTransaction(txid: txid, details: details, context: "CoreService.handleOnchainTransactionReceived")
try BitkitCore.upsertTransactionDetails(detailsList: [coreDetails])
try await self.processOnchainTransaction(txid: txid, details: coreDetails, context: "CoreService.handleOnchainTransactionReceived")
}
}

func handleOnchainTransactionConfirmed(txid: String, details: TransactionDetails) async throws {
func handleOnchainTransactionConfirmed(txid: String, details: LDKNode.TransactionDetails) async throws {
let coreDetails = mapToCoreTransactionDetails(txid: txid, details)

try await ServiceQueue.background(.core) {
try await self.processOnchainTransaction(txid: txid, details: details, context: "CoreService.handleOnchainTransactionConfirmed")
try BitkitCore.upsertTransactionDetails(detailsList: [coreDetails])
try await self.processOnchainTransaction(txid: txid, details: coreDetails, context: "CoreService.handleOnchainTransactionConfirmed")
}
}

Expand Down Expand Up @@ -490,20 +587,18 @@
}

private func processLightningPayment(_ payment: PaymentDetails) async throws {
guard case let .bolt11(hash, preimage, secret, description, bolt11) = payment.kind else { return }

Check warning on line 590 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

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

Check warning on line 590 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

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

// Skip pending inbound payments - just means they created an invoice
guard !(payment.status == .pending && payment.direction == .inbound) else { return }

let paymentTimestamp = UInt64(payment.latestUpdateTimestamp)
let existingActivity = try getActivityById(activityId: payment.id)
let existingLightning: LightningActivity? = if let existingActivity, case let .lightning(ln) = existingActivity { ln } else { nil }

// Skip if existing activity has newer timestamp to avoid overwriting local data
if let existingActivity, case let .lightning(existing) = existingActivity {
let existingUpdatedAt = existing.updatedAt ?? 0
if existingUpdatedAt > paymentTimestamp {
return
}
if let existingUpdatedAt = existingLightning?.updatedAt, existingUpdatedAt > paymentTimestamp {
return
}

let state: BitkitCore.PaymentState = switch payment.status {
Expand All @@ -523,7 +618,8 @@
timestamp: paymentTimestamp,
preimage: preimage,
createdAt: paymentTimestamp,
updatedAt: paymentTimestamp
updatedAt: paymentTimestamp,
seenAt: existingLightning?.seenAt
)

if existingActivity != nil {
Expand All @@ -544,7 +640,7 @@

for payment in payments {
do {
let state: BitkitCore.PaymentState = switch payment.status {

Check warning on line 643 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'state' was never used; consider replacing with '_' or removing it
case .failed:
.failed
case .pending:
Expand Down Expand Up @@ -580,7 +676,7 @@
latestCaughtError = error
}
}
} catch {

Check warning on line 679 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

'catch' block is unreachable because no errors are thrown in 'do' block
Logger.error("Error syncing LDK payment: \(error)", context: "CoreService")
latestCaughtError = error
}
Expand All @@ -598,7 +694,8 @@

/// Marks replacement transactions (with originalTxId in boostTxIds) as doesExist = false when original confirms
/// Finds the channel ID associated with a transaction based on its direction
private func findChannelForTransaction(txid: String, direction: PaymentDirection, transactionDetails: TransactionDetails? = nil) async -> String?
private func findChannelForTransaction(txid: String, direction: PaymentDirection,
transactionDetails: BitkitCore.TransactionDetails? = nil) async -> String?
{
switch direction {
case .inbound:
Expand All @@ -611,13 +708,13 @@
}

/// Check if a transaction spends a closed channel's funding UTXO
private func findClosedChannelForTransaction(txid: String, transactionDetails: TransactionDetails? = nil) async -> String? {
private func findClosedChannelForTransaction(txid: String, transactionDetails: BitkitCore.TransactionDetails? = nil) async -> String? {
do {
let closedChannels = try await getAllClosedChannels(sortDirection: .desc)
guard !closedChannels.isEmpty else { return nil }

// Use provided transaction details if available, otherwise try node
guard let details = transactionDetails ?? LightningService.shared.getTransactionDetails(txid: txid) else {
let details = if let provided = transactionDetails { provided } else { await fetchTransactionDetails(txid: txid) }
guard let details else {
Logger.warn("Transaction details not available for \(txid)", context: "CoreService.findClosedChannelForTransaction")
return nil
}
Expand Down Expand Up @@ -686,7 +783,7 @@
}

/// Check pre-activity metadata for addresses in the transaction
private func findAddressInPreActivityMetadata(details: TransactionDetails, value: UInt64) async -> String? {
private func findAddressInPreActivityMetadata(details: BitkitCore.TransactionDetails, value: UInt64) async -> String? {
for output in details.outputs {
guard let address = output.scriptpubkeyAddress else { continue }
if let metadata = try? await getPreActivityMetadata(searchKey: address, searchByAddress: true),
Expand All @@ -700,9 +797,11 @@
}

/// Find the receiving address for an onchain transaction
private func findReceivingAddress(for txid: String, value: UInt64, transactionDetails: TransactionDetails? = nil) async throws -> String? {
// Use provided transaction details if available, otherwise try node
guard let details = transactionDetails ?? LightningService.shared.getTransactionDetails(txid: txid) else {
private func findReceivingAddress(for txid: String, value: UInt64,
transactionDetails: BitkitCore.TransactionDetails? = nil) async throws -> String?
{
let details = if let provided = transactionDetails { provided } else { await fetchTransactionDetails(txid: txid) }
guard let details else {
Logger.warn("Transaction details not available for \(txid)", context: "CoreService.findReceivingAddress")
return nil
}
Expand Down Expand Up @@ -1048,7 +1147,8 @@
timestamp: timestamp,
preimage: template.status == .succeeded ? "preimage\(activityId)" : nil,
createdAt: timestamp,
updatedAt: timestamp
updatedAt: timestamp,
seenAt: nil
)
)
case .onchain:
Expand All @@ -1071,7 +1171,8 @@
channelId: nil,
transferTxId: nil,
createdAt: timestamp,
updatedAt: timestamp
updatedAt: timestamp,
seenAt: nil
)
)
}
Expand Down
12 changes: 2 additions & 10 deletions Bitkit/Services/LightningService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,19 @@
Logger.debug("Building ldk-node with vssUrl: '\(vssUrl)'")
Logger.debug("Building ldk-node with lnurlAuthServerUrl: '\(lnurlAuthServerUrl)'")

// Create NodeEntropy from mnemonic
let nodeEntropy = NodeEntropy.fromBip39Mnemonic(mnemonic: mnemonic, passphrase: passphrase)
// Set entropy from mnemonic on builder
builder.setEntropyBip39Mnemonic(mnemonic: mnemonic, passphrase: passphrase)

try await ServiceQueue.background(.ldk) {
if !lnurlAuthServerUrl.isEmpty {
self.node = try builder.buildWithVssStore(
nodeEntropy: nodeEntropy,
vssUrl: vssUrl,
storeId: storeId,
lnurlAuthServerUrl: lnurlAuthServerUrl,
fixedHeaders: [:]
)
} else {
self.node = try builder.buildWithVssStoreAndFixedHeaders(
nodeEntropy: nodeEntropy,
vssUrl: vssUrl,
storeId: storeId,
fixedHeaders: [:]
Expand Down Expand Up @@ -488,7 +486,7 @@
}

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

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

View workflow job for this annotation

GitHub Actions / Run Integration Tests

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

Expand Down Expand Up @@ -623,12 +621,6 @@
var channels: [ChannelDetails]? { node?.listChannels() }
var payments: [PaymentDetails]? { node?.listPayments() }

/// Get transaction details from the node for a given transaction ID
/// Returns nil if the transaction is not found in the wallet
func getTransactionDetails(txid: String) -> TransactionDetails? {
return node?.getTransactionDetails(txid: txid)
}

/// Get balance for a specific address in satoshis
/// - Parameter address: The Bitcoin address to check
/// - Returns: The current balance in satoshis
Expand Down Expand Up @@ -691,7 +683,7 @@
onEvent?(event)

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

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

View workflow job for this annotation

GitHub Actions / Run Integration Tests

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

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

View workflow job for this annotation

GitHub Actions / Run Integration Tests

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

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

View workflow job for this annotation

GitHub Actions / Run Integration Tests

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

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

View workflow job for this annotation

GitHub Actions / Run Integration Tests

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

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

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

View workflow job for this annotation

GitHub Actions / Run Integration Tests

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

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

View workflow job for this annotation

GitHub Actions / Run Integration Tests

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

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

View workflow job for this annotation

GitHub Actions / Run Integration Tests

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

// MARK: Balance Events

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

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

View workflow job for this annotation

GitHub Actions / Run Integration Tests

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

Expand Down
Loading
Loading