-
Notifications
You must be signed in to change notification settings - Fork 2
Description
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 callingwatchOrder()watchOrder()callspollUntil()to wait for order state changespollUntil()callsblocktankRepo.getOrder()which returnsResult<IBtOrder>- On network error:
Result.failure→.getOrNull()returnsnull - Code logs "Order not found" (misleading - it's actually a network error)
pollUntil()returnsnullwatchOrder()returnsResult.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
toSpendingtransfers and resume watching them. -
Consider adding a "refresh" button - Allow users to manually trigger a sync of pending transfers if automatic recovery fails.