From 0bbd4a7fffabf6d7d1acfd380d18175d1237f1ac Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 26 Feb 2026 09:50:05 -0300 Subject: [PATCH 1/3] fix: state filter and amount in createTransfersForPaidOrders --- Bitkit/Services/MigrationsService.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Bitkit/Services/MigrationsService.swift b/Bitkit/Services/MigrationsService.swift index 01ceeba1..8980bae6 100644 --- a/Bitkit/Services/MigrationsService.swift +++ b/Bitkit/Services/MigrationsService.swift @@ -2280,14 +2280,16 @@ extension MigrationsService { continue } - if order.state2 == .executed { + // Only create transfers for orders actually paid and awaiting channel + guard order.state2 == .paid else { + Logger.debug("Skipping order \(orderId) with state \(order.state2) for transfer creation", context: "Migration") continue } let transfer = Transfer( id: txId, type: .toSpending, - amountSats: order.clientBalanceSat + order.feeSat, + amountSats: order.clientBalanceSat, channelId: nil, fundingTxId: nil, lspOrderId: orderId, From 977453f78abd014f30d30223936b1b2c15d218aa Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 26 Feb 2026 09:51:22 -0300 Subject: [PATCH 2/3] fix: handle expired orders in syncTransferStates --- Bitkit/Services/TransferService.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Bitkit/Services/TransferService.swift b/Bitkit/Services/TransferService.swift index fdea2e45..1724a946 100644 --- a/Bitkit/Services/TransferService.swift +++ b/Bitkit/Services/TransferService.swift @@ -135,7 +135,17 @@ class TransferService { Logger.debug("Channel \(channelId) exists but not yet usable for transfer: \(transfer.id)", context: "TransferService") } } else { - // No channel ID resolved - check if we should timeout this transfer + // No channel ID resolved - check if order is expired/terminal + if let orderId = transfer.lspOrderId { + if let orders = try? await blocktankService.orders(orderIds: [orderId], filter: nil, refresh: false), + let order = orders.first, + order.state2 == .expired + { + try await markSettled(id: transfer.id) + Logger.info("Order \(orderId) expired, settled transfer: \(transfer.id)", context: "TransferService") + continue + } + } Logger.debug( "Could not resolve channel for transfer: \(transfer.id) orderId: \(transfer.lspOrderId ?? "none")", context: "TransferService" From 3b187e69ce47dd405ad5028c91dd1d57f3d9c421 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 26 Feb 2026 09:54:23 -0300 Subject: [PATCH 3/3] fix: add one-time cleanup for invalid migration transfer --- Bitkit/Services/MigrationsService.swift | 42 +++++++++++++++++++++++++ Bitkit/ViewModels/WalletViewModel.swift | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Bitkit/Services/MigrationsService.swift b/Bitkit/Services/MigrationsService.swift index 8980bae6..c6ecd5c0 100644 --- a/Bitkit/Services/MigrationsService.swift +++ b/Bitkit/Services/MigrationsService.swift @@ -336,6 +336,7 @@ class MigrationsService: ObservableObject { private static let rnPendingChannelMigrationKey = "rnPendingChannelMigration" private static let rnPendingBlocktankOrderIdsKey = "rnPendingBlocktankOrderIds" private static let rnDidAttemptPeerRecoveryKey = "rnDidAttemptMigrationPeerRecovery" + private static let didCleanupInvalidTransfersKey = "didCleanupInvalidMigrationTransfers" @Published var isShowingMigrationLoading = false { didSet { @@ -759,6 +760,47 @@ extension MigrationsService { return true } + /// One-time cleanup for transfers created from unpaid/expired Blocktank orders during migration. + /// The RN backup's paidOrders map could contain orders that were never actually paid. + func cleanupInvalidMigrationTransfers() async { + guard !UserDefaults.standard.bool(forKey: Self.didCleanupInvalidTransfersKey) else { return } + guard rnMigrationCompleted else { return } + + guard let transfers = try? TransferStorage.shared.getActiveTransfers() else { return } + let orderTransfers = transfers.filter { $0.type.isToSpending() && $0.lspOrderId != nil } + + guard !orderTransfers.isEmpty else { + UserDefaults.standard.set(true, forKey: Self.didCleanupInvalidTransfersKey) + return + } + + let orderIds = orderTransfers.compactMap(\.lspOrderId) + + guard let orders = try? await CoreService.shared.blocktank.orders(orderIds: orderIds, filter: nil, refresh: true) else { + // Don't mark as done if we can't reach Blocktank — retry next launch + Logger.warn("Cannot cleanup migration transfers: Blocktank unreachable", context: "Migration") + return + } + + let now = UInt64(Date().timeIntervalSince1970) + for transfer in orderTransfers { + guard let orderId = transfer.lspOrderId, + let order = orders.first(where: { $0.id == orderId }) + else { continue } + + if order.state2 != .paid { + try? TransferStorage.shared.markSettled(id: transfer.id, settledAt: now) + Logger.info( + "Cleanup: settled invalid migration transfer \(transfer.id) for order \(orderId) (state: \(order.state2))", + context: "Migration" + ) + } + } + + UserDefaults.standard.set(true, forKey: Self.didCleanupInvalidTransfersKey) + Logger.info("Migration transfer cleanup completed", context: "Migration") + } + /// Clears all persisted pending migration data from UserDefaults private func clearPendingMigrationData() { pendingChannelMigration = nil diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 95c9033f..8c1cd11d 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -251,7 +251,6 @@ class WalletViewModel: ObservableObject { break } - var info: IBtInfo? do { info = try await coreService.blocktank.info(refresh: true) @@ -658,6 +657,7 @@ class WalletViewModel: ObservableObject { await lightningService.refreshCache() do { + await MigrationsService.shared.cleanupInvalidMigrationTransfers() try? await transferService.syncTransferStates() let state = try await balanceManager.deriveBalanceState() balanceInTransferToSavings = Int(state.balanceInTransferToSavings)