Skip to content

Conversation

@jvsena42
Copy link
Member

@jvsena42 jvsena42 commented Jan 29, 2026

Fixes #745

This PR fixes the "insufficient funds" error that occurs when transferring the maximum balance to spending.

Description

When users tried to transfer their full on-chain balance to Lightning (spending), they would sometimes encounter an "insufficient funds" error even though they had enough sats. This happened because:

  1. Fee estimation mismatch - The LSP fee was estimated using maxSendable amount, but order creation uses a different clientBalance value. Since lspBalance depends on clientBalanceSat, this caused off-by-few-sats discrepancies.

  2. Dust change handling - When the remaining change after the transfer would be below the dust limit (546 sats), the transaction would fail because Bitcoin nodes reject dust outputs.

Changes:

  • Implements two-pass fee estimation to match actual order creation logic
  • Adds dust detection that automatically uses sendAll when remaining change would be below dust limit

Reference: iOS fix PR #416

Preview

Screen_recording_20260129_085609.webm
not-dust.webm

QA Notes

1. MAX transfer to spending

  1. Have on-chain balance (e.g., 5000 sats)
  2. Go to Transfer > To Spending
  3. Press MAX button
  4. Tap Continue
  5. Verify amounts add up correctly on confirm screen
  6. Swipe to transfer
  7. Verify transfer succeeds without "Insufficient funds" error

2. Near-dust change scenarios

  1. Set up a balance where MAX transfer would leave 500-600 sats change
  2. Transfer MAX to spending
  3. Verify sendAll is used (no dust output created)

3. Regression - Partial transfers

  1. Transfer a partial amount (not MAX) to spending
  2. Verify transfer works normally
  3. Verify remaining on-chain balance is correct

@jvsena42 jvsena42 self-assigned this Jan 29, 2026
@claude

This comment has been minimized.

@jvsena42 jvsena42 requested a review from ovitrif January 29, 2026 12:08
@claude

This comment has been minimized.

@claude

This comment has been minimized.

@jvsena42 jvsena42 marked this pull request as draft February 9, 2026 10:27
@jvsena42

This comment was marked as resolved.

@jvsena42 jvsena42 marked this pull request as ready for review February 10, 2026 10:20
@claude

This comment has been minimized.

claude[bot]

This comment was marked as resolved.

Copy link
Collaborator

@ovitrif ovitrif left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added 2 nits and a request for change; pls don't use something1 and something2 naming 🙏🏻

val address = order.payment?.onchain?.address.orEmpty()

// Calculate if change would be dust and we should use sendAll
val spendableBalance =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can be extracted to a private method

Comment on lines +335 to +353
// Two-pass fee estimation to match actual order creation
// First pass: estimate with availableAmount to get approximate clientBalance
val values1 = blocktankRepo.calculateLiquidityOptions(availableAmount).getOrNull()
if (values1 == null) {
_spendingUiState.update { it.copy(isLoading = false) }
return@launch
}
val lspBalance1 = maxOf(values1.defaultLspBalanceSat, values1.minLspBalanceSat)
val feeEstimate1 = blocktankRepo.estimateOrderFee(
spendingBalanceSats = availableAmount,
receivingBalanceSats = _transferValues.value.maxLspBalance
receivingBalanceSats = lspBalance1,
).getOrNull()

if (feeEstimate1 == null) {
_spendingUiState.update { it.copy(isLoading = false) }
return@launch
}

val lspFees1 = feeEstimate1.networkFeeSat.safe() + feeEstimate1.serviceFeeSat.safe()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can also be extracted to a method

Comment on lines +357 to +362
val values2 = blocktankRepo.calculateLiquidityOptions(approxClientBalance).getOrNull()
if (values2 == null || values2.maxLspBalanceSat == 0uL) {
_spendingUiState.update { it.copy(isLoading = false, maxAllowedToSend = 0) }
return@launch
}
val lspBalance2 = maxOf(values2.defaultLspBalanceSat, values2.minLspBalanceSat)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

values1/ values2 is not descriptive enough; makes reader have to actually read implementation to understand what it means

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MAX transfer to spending causes insufficient error

2 participants