-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
More detailed error when HTLC could not be sent (#634)
We use more detailed errors when we fail to send an HTLC, since this may happen because the channel is offline, syncing, opening or closing. It may also happen that we have a channel in the `Normal` state that has enough funds, but we cannot send the HTLC because it hits a channel limit (too many HTLCs pending, a splice is ongoing, etc). When that happens we will retry ignoring the channel, which leads to the default `NoAvailableChannels` error: in that case, the "real" error can be found in the `failures` list of the `OutgoingPaymentFailure` instance. Fixes #616
- Loading branch information
Showing
10 changed files
with
200 additions
and
106 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 82 additions & 33 deletions
115
src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentFailure.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,60 +1,109 @@ | ||
package fr.acinq.lightning.payment | ||
|
||
import fr.acinq.bitcoin.utils.Either | ||
import fr.acinq.lightning.channel.ChannelException | ||
import fr.acinq.lightning.channel.* | ||
import fr.acinq.lightning.db.LightningOutgoingPayment | ||
import fr.acinq.lightning.utils.currentTimestampMillis | ||
import fr.acinq.lightning.wire.* | ||
|
||
/** A fatal failure that stops payment attempts. */ | ||
/** | ||
* A fatal failure that stops payment attempts. | ||
* Applications should define their own localized message for each of these failures. | ||
*/ | ||
sealed class FinalFailure { | ||
|
||
/** Use this function when no payment attempts have been made (e.g. when a precondition failed). */ | ||
fun toPaymentFailure(): OutgoingPaymentFailure = OutgoingPaymentFailure(this, listOf<LightningOutgoingPayment.Part.Status.Failed>()) | ||
|
||
// @formatter:off | ||
object AlreadyPaid : FinalFailure() { override fun toString(): String = "this invoice has already been paid" } | ||
object InvalidPaymentAmount : FinalFailure() { override fun toString(): String = "payment amount must be positive" } | ||
object FeaturesNotSupported : FinalFailure() { override fun toString(): String = "payment request features not supported" } | ||
object InvalidPaymentId : FinalFailure() { override fun toString(): String = "payment ID must be unique" } | ||
object NoAvailableChannels : FinalFailure() { override fun toString(): String = "no channels available to send payment" } | ||
object InsufficientBalance : FinalFailure() { override fun toString(): String = "not enough funds in wallet to afford payment" } | ||
object NoRouteToRecipient : FinalFailure() { override fun toString(): String = "unable to route payment to recipient" } | ||
object RecipientUnreachable : FinalFailure() { override fun toString(): String = "the recipient was offline or did not have enough liquidity to receive the payment" } | ||
object RetryExhausted: FinalFailure() { override fun toString(): String = "payment attempts exhausted without success" } | ||
object WalletRestarted: FinalFailure() { override fun toString(): String = "wallet restarted while a payment was ongoing" } | ||
object UnknownError : FinalFailure() { override fun toString(): String = "an unknown error occurred" } | ||
data object AlreadyPaid : FinalFailure() { override fun toString(): String = "this invoice has already been paid" } | ||
data object InvalidPaymentAmount : FinalFailure() { override fun toString(): String = "payment amount must be positive" } | ||
data object FeaturesNotSupported : FinalFailure() { override fun toString(): String = "payment request features not supported" } | ||
data object InvalidPaymentId : FinalFailure() { override fun toString(): String = "payment ID must be unique" } | ||
data object ChannelNotConnected : FinalFailure() { override fun toString(): String = "channel is not connected yet, please retry when connected" } | ||
data object ChannelOpening : FinalFailure() { override fun toString(): String = "channel creation is in progress, please retry when ready" } | ||
data object ChannelClosing : FinalFailure() { override fun toString(): String = "channel closing is in progress, please retry when a new channel has been created" } | ||
data object NoAvailableChannels : FinalFailure() { override fun toString(): String = "payment could not be sent through existing channels, check individual failures for more details" } | ||
data object InsufficientBalance : FinalFailure() { override fun toString(): String = "not enough funds in wallet to afford payment" } | ||
data object RecipientUnreachable : FinalFailure() { override fun toString(): String = "the recipient was offline or did not have enough liquidity to receive the payment" } | ||
data object RetryExhausted: FinalFailure() { override fun toString(): String = "payment attempts exhausted without success" } | ||
data object WalletRestarted: FinalFailure() { override fun toString(): String = "wallet restarted while a payment was ongoing" } | ||
data object UnknownError : FinalFailure() { override fun toString(): String = "an unknown error occurred" } | ||
// @formatter:on | ||
} | ||
|
||
data class OutgoingPaymentFailure(val reason: FinalFailure, val failures: List<LightningOutgoingPayment.Part.Status.Failed>) { | ||
constructor(reason: FinalFailure, failures: List<Either<ChannelException, FailureMessage>>, completedAt: Long = currentTimestampMillis()) : this(reason, failures.map { convertFailure(it, completedAt) }) | ||
constructor(reason: FinalFailure, failures: List<Either<ChannelException, FailureMessage>>, completedAt: Long = currentTimestampMillis()) : this( | ||
reason, | ||
failures.map { LightningOutgoingPayment.Part.Status.Failed(convertFailure(it), completedAt) } | ||
) | ||
|
||
/** Extracts the most user-friendly reason for the payment failure. */ | ||
fun explain(): Either<LightningOutgoingPayment.Part.Status.Failure, FinalFailure> { | ||
val partFailure = failures.map { it.failure }.lastOrNull { it !is LightningOutgoingPayment.Part.Status.Failure.Uninterpretable } ?: failures.lastOrNull()?.failure | ||
return when (reason) { | ||
FinalFailure.NoAvailableChannels, FinalFailure.UnknownError, FinalFailure.RetryExhausted -> partFailure?.let { Either.Left(it) } ?: Either.Right(reason) | ||
else -> Either.Right(reason) | ||
} | ||
} | ||
|
||
/** | ||
* A detailed summary of the all internal errors. | ||
* This is targeted at users with technical knowledge of the lightning protocol. | ||
*/ | ||
fun details(): String = failures.foldIndexed("") { index, msg, problem -> msg + "${index + 1}: ${problem.details}\n" } | ||
fun details(): String = failures.foldIndexed("") { index, msg, problem -> msg + "${index + 1}: ${problem.failure}\n" } | ||
|
||
companion object { | ||
fun convertFailure(failure: Either<ChannelException, FailureMessage>, completedAt: Long = currentTimestampMillis()): LightningOutgoingPayment.Part.Status.Failed = when (failure) { | ||
is Either.Left -> LightningOutgoingPayment.Part.Status.Failed(null, failure.value.details(), completedAt) | ||
is Either.Right -> LightningOutgoingPayment.Part.Status.Failed(failure.value.code, failure.value.message, completedAt) | ||
} | ||
|
||
fun isRouteError(failure: LightningOutgoingPayment.Part.Status.Failed) = when (failure.remoteFailureCode) { | ||
UnknownNextPeer.code -> true | ||
ChannelDisabled.code -> true | ||
TemporaryChannelFailure.code -> true | ||
PermanentChannelFailure.code -> true | ||
TemporaryNodeFailure.code -> true | ||
PermanentNodeFailure.code -> true | ||
else -> false | ||
} | ||
|
||
fun isRejectedByRecipient(failure: LightningOutgoingPayment.Part.Status.Failed) = when (failure.remoteFailureCode) { | ||
IncorrectOrUnknownPaymentDetails.code -> true | ||
else -> false | ||
fun convertFailure(failure: Either<ChannelException, FailureMessage>): LightningOutgoingPayment.Part.Status.Failure { | ||
return when (failure) { | ||
is Either.Left -> when (failure.value) { | ||
is HtlcValueTooSmall -> LightningOutgoingPayment.Part.Status.Failure.PaymentAmountTooSmall | ||
is CannotAffordFees -> LightningOutgoingPayment.Part.Status.Failure.PaymentAmountTooBig | ||
is RemoteCannotAffordFeesForNewHtlc -> LightningOutgoingPayment.Part.Status.Failure.PaymentAmountTooBig | ||
is HtlcValueTooHighInFlight -> LightningOutgoingPayment.Part.Status.Failure.PaymentAmountTooBig | ||
is InsufficientFunds -> LightningOutgoingPayment.Part.Status.Failure.NotEnoughFunds | ||
is TooManyAcceptedHtlcs -> LightningOutgoingPayment.Part.Status.Failure.TooManyPendingPayments | ||
is TooManyOfferedHtlcs -> LightningOutgoingPayment.Part.Status.Failure.TooManyPendingPayments | ||
is ExpiryTooBig -> LightningOutgoingPayment.Part.Status.Failure.PaymentExpiryTooBig | ||
is ForbiddenDuringSplice -> LightningOutgoingPayment.Part.Status.Failure.ChannelIsSplicing | ||
is ChannelUnavailable -> LightningOutgoingPayment.Part.Status.Failure.ChannelIsClosing | ||
is ClosingAlreadyInProgress -> LightningOutgoingPayment.Part.Status.Failure.ChannelIsClosing | ||
is ForcedLocalCommit -> LightningOutgoingPayment.Part.Status.Failure.ChannelIsClosing | ||
is FundingTxSpent -> LightningOutgoingPayment.Part.Status.Failure.ChannelIsClosing | ||
is HtlcOverriddenByLocalCommit -> LightningOutgoingPayment.Part.Status.Failure.ChannelIsClosing | ||
is HtlcsTimedOutDownstream -> LightningOutgoingPayment.Part.Status.Failure.ChannelIsClosing | ||
is NoMoreHtlcsClosingInProgress -> LightningOutgoingPayment.Part.Status.Failure.ChannelIsClosing | ||
else -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
} | ||
is Either.Right -> when (failure.value) { | ||
is AmountBelowMinimum -> LightningOutgoingPayment.Part.Status.Failure.PaymentAmountTooSmall | ||
is FeeInsufficient -> LightningOutgoingPayment.Part.Status.Failure.NotEnoughFees | ||
TrampolineExpiryTooSoon -> LightningOutgoingPayment.Part.Status.Failure.NotEnoughFees | ||
TrampolineFeeInsufficient -> LightningOutgoingPayment.Part.Status.Failure.NotEnoughFees | ||
is FinalIncorrectCltvExpiry -> LightningOutgoingPayment.Part.Status.Failure.RecipientRejectedPayment | ||
is FinalIncorrectHtlcAmount -> LightningOutgoingPayment.Part.Status.Failure.RecipientRejectedPayment | ||
is IncorrectOrUnknownPaymentDetails -> LightningOutgoingPayment.Part.Status.Failure.RecipientRejectedPayment | ||
PaymentTimeout -> LightningOutgoingPayment.Part.Status.Failure.RecipientLiquidityIssue | ||
UnknownNextPeer -> LightningOutgoingPayment.Part.Status.Failure.RecipientIsOffline | ||
is ExpiryTooSoon -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure | ||
ExpiryTooFar -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure | ||
is ChannelDisabled -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure | ||
is TemporaryChannelFailure -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure | ||
TemporaryNodeFailure -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure | ||
PermanentChannelFailure -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure | ||
PermanentNodeFailure -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure | ||
is InvalidOnionBlinding -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
is InvalidOnionHmac -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
is InvalidOnionKey -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
is InvalidOnionPayload -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
is InvalidOnionVersion -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
InvalidRealm -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
is IncorrectCltvExpiry -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
RequiredChannelFeatureMissing -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
RequiredNodeFeatureMissing -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
is UnknownFailureMessage -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message) | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.