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
40 changes: 38 additions & 2 deletions Bitkit/Services/CoreService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@

var isConfirmed = false
var confirmedTimestamp: UInt64?
if case let .confirmed(blockHash, height, blockTimestamp) = txStatus {

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

View workflow job for this annotation

GitHub Actions / Run Tests

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

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

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'blockHash' was never used; consider replacing with '_' or removing it
isConfirmed = true
confirmedTimestamp = blockTimestamp
}
Expand All @@ -145,12 +145,20 @@
}()
let preservedIsBoosted = existingOnchain?.isBoosted ?? false
let preservedBoostTxIds = existingOnchain?.boostTxIds ?? []
let preservedIsTransfer = existingOnchain?.isTransfer ?? false
let preservedChannelId = existingOnchain?.channelId
var preservedIsTransfer = existingOnchain?.isTransfer ?? false
var preservedChannelId = existingOnchain?.channelId
let preservedTransferTxId = existingOnchain?.transferTxId
let preservedFeeRate = existingOnchain?.feeRate ?? 1
let preservedAddress = existingOnchain?.address ?? "Loading..."

// Check if this transaction is a channel close by checking if it spends a closed channel's funding UTXO
if payment.direction == .inbound && (preservedChannelId == nil || !preservedIsTransfer) {
if let channelId = await self.findClosedChannelForTransaction(txid: txid) {
preservedChannelId = channelId
preservedIsTransfer = true
}
}

// Check if this is a replacement transaction (RBF) that should be marked as boosted
let isReplacementTransaction = ActivityService.replacementTransactions.keys.contains(txid)
let shouldMarkAsBoosted = preservedIsBoosted || isReplacementTransaction
Expand Down Expand Up @@ -231,7 +239,7 @@
print(payment)
addedCount += 1
}
} else if case let .bolt11(hash, preimage, secret, description, bolt11) = payment.kind {

Check warning on line 242 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 { continue }

Expand Down Expand Up @@ -288,6 +296,34 @@
}
}

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

let txDetails = try await AddressChecker.getTransaction(txid: txid)

// Check if any input spends a closed channel's funding UTXO
for input in txDetails.vin {
guard let inputTxid = input.txid, let inputVout = input.vout else { continue }

if let matchingChannel = closedChannels.first(where: { channel in
channel.fundingTxoTxid == inputTxid && channel.fundingTxoIndex == UInt32(inputVout)
}) {
return matchingChannel.channelId
}
}
} catch {
Logger.warn(
"Failed to check if transaction \(txid) spends closed channel funding UTXO: \(error)",
context: "CoreService.findClosedChannelForTransaction"
)
}

return nil
}

/// Check pre-activity metadata for addresses in the transaction
private func findAddressInPreActivityMetadata(txDetails: TxDetails, value: UInt64) async -> String? {
for output in txDetails.vout {
Expand Down
14 changes: 11 additions & 3 deletions Bitkit/Services/LightningService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,10 @@
}

await MainActor.run {
channelCache = Dictionary(uniqueKeysWithValues: (channels ?? []).map { ($0.channelId.description, $0) })
Logger.debug("Refreshed channel cache: \(channelCache.count) channels", context: "LightningService")
let newChannels = Dictionary(uniqueKeysWithValues: (channels ?? []).map { ($0.channelId.description, $0) })
for (key, value) in newChannels {
channelCache[key] = value
}
}
}

Expand Down Expand Up @@ -410,7 +412,7 @@
}

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

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

View workflow job for this annotation

GitHub Actions / Run Tests

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

Expand Down Expand Up @@ -569,15 +571,15 @@

// TODO: actual event handler
switch event {
case let .paymentSuccessful(paymentId, paymentHash, paymentPreimage, feePaidMsat):

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

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'paymentPreimage' was never used; consider replacing with '_' or removing it
Logger.info("✅ Payment successful: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash) feePaidMsat: \(feePaidMsat ?? 0)")
case let .paymentFailed(paymentId, paymentHash, reason):
Logger.info(
"❌ Payment failed: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash ?? "") reason: \(reason.debugDescription)"
)
case let .paymentReceived(paymentId, paymentHash, amountMsat, feePaidMsat):

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

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'feePaidMsat' was never used; consider replacing with '_' or removing it
Logger.info("🤑 Payment received: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash) amountMsat: \(amountMsat)")
case let .paymentClaimable(paymentId, paymentHash, claimableAmountMsat, claimDeadline, customRecords):

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

View workflow job for this annotation

GitHub Actions / Run Tests

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

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

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'claimDeadline' was never used; consider replacing with '_' or removing it
Logger.info(
"🫰 Payment claimable: paymentId: \(paymentId) paymentHash: \(paymentHash) claimableAmountMsat: \(claimableAmountMsat)"
)
Expand All @@ -602,8 +604,14 @@

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

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

View workflow job for this annotation

GitHub Actions / Run Tests

result of call to 'run(resultType:body:)' is unused
channelCache.removeValue(forKey: channelIdString)
}
} else {
Logger.error("Could not find channel details for closed channel: \(userChannelId) in cache", context: "LightningService")
Logger.error(
"Could not find channel details for closed channel: channelId=\(channelIdString) userChannelId=\(userChannelId) in cache",
context: "LightningService"
)
}
case .paymentForwarded:
break
Expand Down
10 changes: 8 additions & 2 deletions Bitkit/Views/Wallets/Activity/ActivityIcon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,16 @@ struct ActivityIcon: View {
)
} else {
let paymentIcon = txType == PaymentType.sent ? "arrow-up" : "arrow-down"
let (iconColor, backgroundColor): (Color, Color) = if isTransfer {
// From savings (to spending) = sent = orange, From spending (to savings) = received = purple
txType == .sent ? (.brandAccent, .brand16) : (.purpleAccent, .purple16)
} else {
(.brandAccent, .brand16)
}
CircularIcon(
icon: isTransfer ? "arrow-up-down" : paymentIcon,
iconColor: .brandAccent,
backgroundColor: .brand16,
iconColor: iconColor,
backgroundColor: backgroundColor,
size: size
)
}
Expand Down
14 changes: 11 additions & 3 deletions Bitkit/Views/Wallets/Activity/ActivityRowOnchain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,31 @@ struct ActivityRowOnchain: View {
return DateFormatterHelpers.getActivityItemDate(item.timestamp)
}

private var feeDescription: String {
TransactionSpeed.getFeeDescription(feeRate: item.feeRate, feeEstimates: feeEstimates)
}

private var durationWithoutSymbol: String {
// Remove ± symbol since localization strings already include it
feeDescription.replacingOccurrences(of: "±", with: "")
}

private var description: String {
if item.isTransfer {
switch item.txType {
case .sent:
return item.confirmed ?
t("wallet__activity_transfer_spending_done") :
t("wallet__activity_transfer_spending_pending", variables: ["duration": "TODO"])
t("wallet__activity_transfer_spending_pending", variables: ["duration": durationWithoutSymbol])
case .received:
return item.confirmed ?
t("wallet__activity_transfer_savings_done") :
t("wallet__activity_transfer_savings_pending", variables: ["duration": "TODO"])
t("wallet__activity_transfer_savings_pending", variables: ["duration": durationWithoutSymbol])
}
} else {
if item.confirmed {
return formattedTime
} else {
let feeDescription = TransactionSpeed.getFeeDescription(feeRate: item.feeRate, feeEstimates: feeEstimates)
return t("wallet__activity_confirms_in", variables: ["feeRateDescription": feeDescription])
}
}
Expand Down
Loading