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
onTransferToSpendingConfirm() launches coroutine calling watchOrder()
watchOrder() calls pollUntil() to wait for order state changes
pollUntil() calls blocktankRepo.getOrder() which returns Result<IBtOrder>
- On network error:
Result.failure → .getOrNull() returns null
- Code logs "Order not found" (misleading - it's actually a network error)
pollUntil() returns null
watchOrder() returns Result.failure(Exception("Order not found or expired"))
- Coroutine completes, polling stops permanently
- 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
- 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)
}
}
-
Resume watching pending transfers on app foreground - When the app returns to foreground, check for any pending toSpending transfers and resume watching them.
-
Consider adding a "refresh" button - Allow users to manually trigger a sync of pending transfers if automatic recovery fails.
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.ktin thepollUntilfunction. WhengetOrder()fails due to a network error,.getOrNull()returnsnull, which is misinterpreted as "order not found" and causes the polling loop to exit permanently.Relevant code (
TransferViewModel.kt:267-283):Bug Flow
onTransferToSpendingConfirm()launches coroutine callingwatchOrder()watchOrder()callspollUntil()to wait for order state changespollUntil()callsblocktankRepo.getOrder()which returnsResult<IBtOrder>Result.failure→.getOrNull()returnsnullpollUntil()returnsnullwatchOrder()returnsResult.failure(Exception("Order not found or expired"))Key Issue
The code conflates two different error conditions:
Using
.getOrNull()loses the distinction between these cases.Recommended Fix
Resume watching pending transfers on app foreground - When the app returns to foreground, check for any pending
toSpendingtransfers and resume watching them.Consider adding a "refresh" button - Allow users to manually trigger a sync of pending transfers if automatic recovery fails.