Skip to content

Spending balance remains 0 after opening the channel from the app, despite transfer to spending confirmed #338

Description

@piotr-iohk

To reproduce

  • Have onchain balance
  • Transfer to spending
  • Put app in background during the channel opening process (or have temporary network issues)

Expected

Channel is opened and spending balance is updated.

Actual

Transfer is confirmed, but the channel remains in pending state, spending balance remains 0 in the app indefinitely. Restarting the app resolves the issue.

Logs & Details

bitkit_logs_2026-01-15_11-13-58.zip

Channel details: https://synonymworkspace.slack.com/archives/C0726U60V6Y/p1768473770381519

blocktank-mainnet1  [11:42 AM]
:information_source: info: Created order 929f3d51-d8af-4979-b7c8-c992b316ff26
lspBalanceSat=529124, clientBalanceSat=12226, source=bitkit, discountCode=undefined
[11:42 AM] :information_source: info: Order 929f3d51-d8af-4979-b7c8-c992b316ff26
Onchain tx update
3dad74e164291e95dccb278d4412fb6c743a5cce7d4b0280bf90352563afaa0c:0 14532sat, confirmations: 0, 0conf invalid: rbf.
blocktank-mainnet1  [11:55 AM]
:information_source: info: Order 929f3d51-d8af-4979-b7c8-c992b316ff26
Onchain tx update
3dad74e164291e95dccb278d4412fb6c743a5cce7d4b0280bf90352563afaa0c:0 14532sat, confirmations: 1, 0conf invalid: rbf.

AI Analysis (Claude)

Root Cause

The bug is in TransferViewModel.swift in the watchOrder function. When order polling fails due to a network error, stopPolling() is called and never resumes, even after network connectivity is restored.

Relevant code (TransferViewModel.swift:226-229):

} catch {
    Logger.error(error, context: "Failed to watch order")
    stopPolling()  // BUG: Permanently stops watching on ANY error
}

Timeline from Logs

Time (UTC) Event
10:42:51 App creates transfer, starts watching order 929f3d51
10:52:04 App goes to background
10:55:00 Blocktank confirms funding tx (1 conf)
10:56:48 App returns from background, order poll fails with HTTP error
10:56:48 stopPolling() called - order watching stops permanently
10:56:48 "The Internet connection appears to be offline" errors
10:58:45 LDK node restarts, but order watching never resumes
11:13:52 Logs end - app still shows lightning=0, Available channels: 0

Key Log Evidence

[2026-01-15 10:56:48.177 UTC] ERROR❌: HTTP client error: error sending request for url (https://api1.blocktank.to/api/channels?ids%5B%5D=929f3d51-d8af-4979-b7c8-c992b316ff26)
[2026-01-15 10:56:48.178 UTC] ERROR❌: Failed to watch order [TransferViewModel.swift: watchOrder(orderId:frequencyMs:) line: 227]

After this error, no further "Refreshing order" logs appear, confirming polling stopped.

Contributing Factor

The Lightning channel handshake also requires the LDK node to be online. The app was going in/out of background with network instability, preventing the channel opening handshake from completing until the user manually restarted the app.

Recommended Fix

  1. Don't stop polling on transient errors - Implement retry logic with exponential backoff instead of calling stopPolling() on any error:
} catch {
    Logger.error(error, context: "Failed to watch order, will retry")
    // Don't call stopPolling() - let the timer continue
    // Optionally track error count and only stop after N consecutive failures
}
  1. Resume watching pending transfers on app foreground - When the app returns to foreground, check for any pending toSpending transfers and resume watching them:
// In ScenePhase or AppViewModel when app becomes active
if let pendingTransfer = transferService.getPendingToSpendingTransfer() {
    watchOrder(orderId: pendingTransfer.orderId)
}
  1. Consider adding a "refresh" button - Allow users to manually trigger a sync of pending transfers if automatic recovery fails.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions