Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
42 changes: 42 additions & 0 deletions Bitkit/Extensions/ChannelDetails+Extensions.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BitkitCore
import Foundation
import LDKNode

Expand All @@ -6,6 +7,47 @@ extension ChannelDetails {
var spendableBalanceSats: UInt64 {
return outboundCapacityMsat / 1000 + (unspendablePunishmentReserve ?? 0)
}

/// Find the linked Blocktank order for this channel
/// - Parameter orders: Array of Blocktank orders to search
/// - Returns: The matching order if found, nil otherwise
func findLinkedOrder(in orders: [IBtOrder]) -> IBtOrder? {
// Match by userChannelId (which is set to order.id for Blocktank orders)
if let order = orders.first(where: { $0.id == userChannelId }) {
return order
}

// Match by short channel ID
if let shortChannelId {
let shortChannelIdString = String(shortChannelId)
if let order = orders.first(where: { order in
order.channel?.shortChannelId == shortChannelIdString
}) {
return order
}
}

// Match by funding transaction
if let fundingTxo {
if let order = orders.first(where: { order in
order.channel?.fundingTx.id == fundingTxo.txid
}) {
return order
}
}

// Match by counterparty node ID (less reliable, could match multiple)
let counterpartyNodeIdString = counterpartyNodeId.description
if let order = orders.first(where: { order in
guard let orderChannel = order.channel else { return false }
return orderChannel.clientNodePubkey == counterpartyNodeIdString ||
orderChannel.lspNodePubkey == counterpartyNodeIdString
}) {
return order
}

return nil
}
}

// MARK: - Mock Data
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,7 @@
"wallet__activity_transfer_spending_pending" = "From Savings (±{duration})";
"wallet__activity_transfer_spending_done" = "From Savings";
"wallet__activity_transfer_to_spending" = "To Spending";
"wallet__activity_transfer_to_savings" = "To Savings";
"wallet__activity_transfer_pending" = "Transfer (±{duration})";
"wallet__activity_confirms_in" = "Confirms in {feeRateDescription}";
"wallet__activity_confirms_in_boosted" = "Boosting. Confirms in {feeRateDescription}";
Expand All @@ -1023,6 +1024,7 @@
"wallet__activity_removed_msg" = "Please check your activity list. The {count} impacted transaction(s) will be highlighted in red.";
"wallet__activity_boosting" = "Boosting";
"wallet__activity_fee" = "Fee";
"wallet__activity_fee_prepaid" = "Fee (Prepaid)";
"wallet__activity_payment" = "Payment";
"wallet__activity_status" = "Status";
"wallet__activity_date" = "Date";
Expand Down
56 changes: 53 additions & 3 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 @@ -151,9 +151,17 @@
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) {
// Check if this transaction is a channel transfer (open or close)
if preservedChannelId == nil || !preservedIsTransfer {
let channelId: String? = if payment.direction == .inbound {
// Check if this transaction is a channel close by checking if it spends a closed channel's funding UTXO
await self.findClosedChannelForTransaction(txid: txid)
} else {
// Check if this transaction is a channel open by checking if it's the funding transaction for an open channel
await self.findOpenChannelForTransaction(txid: txid)
}

if let channelId {
preservedChannelId = channelId
preservedIsTransfer = true
}
Expand Down Expand Up @@ -239,7 +247,7 @@
print(payment)
addedCount += 1
}
} else if case let .bolt11(hash, preimage, secret, description, bolt11) = payment.kind {

Check warning on line 250 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 250 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 @@ -324,6 +332,48 @@
return nil
}

/// Check if a transaction is the funding transaction for an open channel
private func findOpenChannelForTransaction(txid: String) async -> String? {
guard let channels = LightningService.shared.channels, !channels.isEmpty else {
return nil
}

// First, check if the transaction matches any channel's funding transaction directly
if let channel = channels.first(where: { $0.fundingTxo?.txid.description == txid }) {
return channel.channelId.description
}

// If no direct match, check Blocktank orders for payment transactions
do {
let orders = try await coreService.blocktank.orders(orderIds: nil, filter: nil, refresh: false)

// Find order with matching payment transaction
guard let order = orders.first(where: { order in
order.payment?.onchain?.transactions.contains { $0.txId == txid } ?? false
}) else {
return nil
}

// Find channel that matches this order's channel funding transaction
guard let orderChannel = order.channel else {
return nil
}

if let channel = channels.first(where: { channel in
channel.fundingTxo?.txid.description == orderChannel.fundingTx.id
}) {
return channel.channelId.description
}
} catch {
Logger.warn(
"Failed to fetch Blocktank orders: \(error)",
context: "CoreService.findOpenChannelForTransaction"
)
}

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
65 changes: 14 additions & 51 deletions Bitkit/ViewModels/TransferViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,20 @@ class TransferViewModel: ObservableObject {
// Create transfer tracking record for spending
do {
// Create pre-activity metadata for the transfer transaction
await createTransferMetadata(txId: txid, feeRate: UInt64(satsPerVbyte), address: address)
let currentTime = UInt64(Date().timeIntervalSince1970)
let preActivityMetadata = BitkitCore.PreActivityMetadata(
paymentId: txid,
tags: [],
paymentHash: nil,
txId: txid,
address: address,
isReceive: false,
feeRate: UInt64(satsPerVbyte),
isTransfer: true,
channelId: nil,
createdAt: currentTime
)
try? await coreService.activity.addPreActivityMetadata(preActivityMetadata)

let transferId = try await transferService.createTransfer(
type: .toSpending,
Expand Down Expand Up @@ -411,11 +424,6 @@ class TransferViewModel: ObservableObject {

// Create transfer tracking record with the ACTUAL channel ID (not user channel ID)
do {
// Create pre-activity metadata for the transfer transaction if we have a fundingTxId
if let fundingTxId {
await createTransferMetadata(txId: fundingTxId, channelId: actualChannelId)
}

let transferId = try await transferService.createTransfer(
type: .toSpending,
amountSats: amountSats,
Expand Down Expand Up @@ -692,51 +700,6 @@ class TransferViewModel: ObservableObject {
)
}
}

/// Create pre-activity metadata for a transfer transaction, or update existing activity if it already exists
private func createTransferMetadata(txId: String, channelId: String? = nil, feeRate: UInt64? = nil, address: String? = nil) async {
// Check if activity already exists (may have been synced from LDK while waiting for channel pending event)
do {
let activities = try await coreService.activity.get(filter: .onchain, limit: 20)
if let existingActivity = activities.first(where: { activity in
guard case let .onchain(onchain) = activity else { return false }
return onchain.txId == txId
}),
case var .onchain(onchain) = existingActivity
{
// Activity already exists, update it directly with transfer metadata
onchain.isTransfer = true
onchain.channelId = channelId
if let feeRate, feeRate > 0 {
onchain.feeRate = feeRate
}
if let address, !address.isEmpty {
onchain.address = address
}
try? await coreService.activity.update(id: onchain.id, activity: .onchain(onchain))
Logger.info("Updated existing activity \(onchain.id) with transfer metadata", context: "TransferViewModel")
return
}
} catch {
Logger.warn("Failed to check for existing activity: \(error)", context: "TransferViewModel")
}

// Activity doesn't exist yet, create pre-activity metadata (will be applied on insert)
let currentTime = UInt64(Date().timeIntervalSince1970)
let preActivityMetadata = BitkitCore.PreActivityMetadata(
paymentId: txId,
tags: [],
paymentHash: nil,
txId: txId,
address: address,
isReceive: false,
feeRate: feeRate ?? 0,
isTransfer: true,
channelId: channelId,
createdAt: currentTime
)
try? await coreService.activity.addPreActivityMetadata(preActivityMetadata)
}
}

/// Actor to safely capture channel data from channel pending events
Expand Down
44 changes: 1 addition & 43 deletions Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -356,49 +356,7 @@ struct LightningConnectionsView: View {

private func findLinkedOrder(for channel: ChannelDetails) -> IBtOrder? {
guard let orders = blocktank.orders else { return nil }

// For fake channels created from orders, match by userChannelId (which we set to order.id)
for order in orders {
if order.id == channel.userChannelId {
return order
}
}

// Try to match by short channel ID first (most reliable)
if let shortChannelId = channel.shortChannelId {
let shortChannelIdString = String(shortChannelId)
for order in orders {
if let orderChannel = order.channel,
let orderShortChannelId = orderChannel.shortChannelId,
orderShortChannelId == shortChannelIdString
{
return order
}
}
}

// Try to match by funding transaction if available
if let fundingTxo = channel.fundingTxo {
for order in orders {
if let orderChannel = order.channel,
orderChannel.fundingTx.id == fundingTxo.txid
{
return order
}
}
}

// Try to match by counterparty node ID (less reliable, could match multiple)
let counterpartyNodeIdString = channel.counterpartyNodeId.description
for order in orders {
if let orderChannel = order.channel,
orderChannel.clientNodePubkey == counterpartyNodeIdString || orderChannel.lspNodePubkey == counterpartyNodeIdString
{
return order
}
}

return nil
return channel.findLinkedOrder(in: orders)
}

private func formatNumber(_ number: UInt64) -> String {
Expand Down
Loading
Loading