Skip to content

Spending balance remains 0 after opening the channel from the app, despite transfer to spending confirmed (Android) #608

@piotr-iohk

Description

@piotr-iohk

I observed the same situation as in iOS from here: synonymdev/bitkit-ios#338, (no logs, as the channel eventually opened and we thought there was some blocktank hiccup)

Pasting AI analysis on the potential root cause.


AI Analysis (Claude)

Root Cause

The bug is in TransferViewModel.kt in the pollUntil function. When getOrder() fails due to a network error, .getOrNull() returns null, which is misinterpreted as "order not found" and causes the polling loop to exit permanently.

Relevant code (TransferViewModel.kt:267-283):

private suspend fun pollUntil(orderId: String, condition: (IBtOrder) -> Boolean): IBtOrder? {
    while (true) {
        val order = blocktankRepo.getOrder(orderId, refresh = true).getOrNull()
        if (order == null) {
            Logger.error("Order not found: '$orderId'", context = TAG)
            return null  // BUG: Network errors also cause null, exits loop permanently
        }
        if (order.state2 == BtOrderState2.EXPIRED) {
            Logger.error("Order expired: '$orderId'", context = TAG)
            return null
        }
        if (condition(order)) {
            return order
        }
        delay(POLL_INTERVAL_MS)
    }
}

Bug Flow

  1. onTransferToSpendingConfirm() launches coroutine calling watchOrder()
  2. watchOrder() calls pollUntil() to wait for order state changes
  3. pollUntil() calls blocktankRepo.getOrder() which returns Result<IBtOrder>
  4. On network error: Result.failure.getOrNull() returns null
  5. Code logs "Order not found" (misleading - it's actually a network error)
  6. pollUntil() returns null
  7. watchOrder() returns Result.failure(Exception("Order not found or expired"))
  8. Coroutine completes, polling stops permanently
  9. Channel opens on Blocktank's side, but app never detects it

Key Issue

The code conflates two different error conditions:

  • Actual "order not found": Should stop polling (order doesn't exist)
  • Network error: Should retry (transient failure)

Using .getOrNull() loses the distinction between these cases.

Recommended Fix

  1. Distinguish between network errors and actual "not found":
private suspend fun pollUntil(orderId: String, condition: (IBtOrder) -> Boolean): IBtOrder? {
    var consecutiveErrors = 0
    val maxConsecutiveErrors = 5
    
    while (true) {
        val result = blocktankRepo.getOrder(orderId, refresh = true)
        
        result.fold(
            onSuccess = { order ->
                consecutiveErrors = 0  // Reset on success
                
                if (order.state2 == BtOrderState2.EXPIRED) {
                    Logger.error("Order expired: '$orderId'", context = TAG)
                    return null
                }
                if (condition(order)) {
                    return order
                }
            },
            onFailure = { error ->
                consecutiveErrors++
                Logger.warn("Failed to fetch order (attempt $consecutiveErrors): ${error.message}", context = TAG)
                
                if (consecutiveErrors >= maxConsecutiveErrors) {
                    Logger.error("Too many consecutive errors, giving up", context = TAG)
                    return null
                }
                // Continue polling on transient errors
            }
        )
        
        delay(POLL_INTERVAL_MS)
    }
}
  1. Resume watching pending transfers on app foreground - When the app returns to foreground, check for any pending toSpending transfers and resume watching them.

  2. 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

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions