diff --git a/Bitkit/Services/MigrationsService.swift b/Bitkit/Services/MigrationsService.swift index 01ceeba1..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 @@ -2280,14 +2322,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, 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" 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)