-
Notifications
You must be signed in to change notification settings - Fork 4
feat(send): improve Lightning send failure recovery #1140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
776366e
48391ce
f3f2ac0
e9340dd
a77fe99
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,15 +3,111 @@ package to.bitkit.ext | |
| import android.content.Context | ||
| import org.lightningdevkit.ldknode.PaymentFailureReason | ||
| import to.bitkit.R | ||
| import to.bitkit.models.SendFailureDetails | ||
| import to.bitkit.utils.LdkError | ||
|
|
||
| fun PaymentFailureReason?.toUserMessage(context: Context): String = when (this) { | ||
| PaymentFailureReason.RECIPIENT_REJECTED -> | ||
| context.getString(R.string.wallet__toast_payment_failed_recipient_rejected) | ||
| context.getString(R.string.wallet__payment_recipient_rejected) | ||
| PaymentFailureReason.USER_ABANDONED -> | ||
| context.getString(R.string.wallet__payment_abandoned) | ||
| PaymentFailureReason.RETRIES_EXHAUSTED -> | ||
| context.getString(R.string.wallet__toast_payment_failed_retries_exhausted) | ||
| context.getString(R.string.wallet__payment_retries_exhausted) | ||
| PaymentFailureReason.ROUTE_NOT_FOUND -> | ||
| context.getString(R.string.wallet__toast_payment_failed_route_not_found) | ||
| context.getString(R.string.wallet__payment_route_not_found) | ||
| PaymentFailureReason.PAYMENT_EXPIRED -> | ||
| context.getString(R.string.wallet__toast_payment_failed_timeout) | ||
| else -> context.getString(R.string.wallet__toast_payment_failed_description) | ||
| context.getString(R.string.wallet__payment_expired) | ||
| PaymentFailureReason.UNKNOWN_REQUIRED_FEATURES -> | ||
| context.getString(R.string.wallet__payment_unknown_required_features) | ||
| PaymentFailureReason.INVOICE_REQUEST_EXPIRED -> | ||
| context.getString(R.string.wallet__payment_invoice_request_expired) | ||
| PaymentFailureReason.INVOICE_REQUEST_REJECTED -> | ||
| context.getString(R.string.wallet__payment_invoice_request_rejected) | ||
| else -> context.getString(R.string.wallet__payment_failed_description) | ||
| } | ||
|
|
||
| fun PaymentFailureReason?.shouldResetRoutingCachesOnRetry(): Boolean = | ||
| this == PaymentFailureReason.ROUTE_NOT_FOUND || this == PaymentFailureReason.RETRIES_EXHAUSTED | ||
|
|
||
| fun PaymentFailureReason?.toCompactFailureType(): String { | ||
| return this?.name?.snakeToLowerCamel() ?: UNKNOWN_FAILURE_TYPE | ||
| } | ||
|
|
||
| fun PaymentFailureReason?.toSendFailureDetails( | ||
| context: Context, | ||
| paymentRequest: String? = null, | ||
| ): SendFailureDetails { | ||
| return SendFailureDetails( | ||
| message = toUserMessage(context), | ||
| failureType = toCompactFailureType(), | ||
| resetRoutingCachesOnRetry = shouldResetRoutingCachesOnRetry(), | ||
| paymentRequest = paymentRequest, | ||
| ) | ||
| } | ||
|
|
||
| fun Throwable.toSendFailureMessage(context: Context): String { | ||
| val fallbackMessage = context.getString(R.string.wallet__payment_failed_description) | ||
| val rawMessage = message?.trim().orEmpty() | ||
|
|
||
| if (this is LdkError || rawMessage.isBlank() || rawMessage.looksInternalPaymentError()) { | ||
| return fallbackMessage | ||
| } | ||
|
|
||
| return rawMessage | ||
| } | ||
|
|
||
| fun Throwable.toCompactFailureType(): String { | ||
| val rawValue = message?.trim()?.takeIf { it.isNotEmpty() } | ||
| ?: this::class.simpleName | ||
| ?: UNKNOWN_FAILURE_TYPE | ||
|
|
||
| return rawValue.compactFailureType() | ||
| } | ||
|
|
||
| fun Throwable.toSendFailureDetails( | ||
| context: Context, | ||
| paymentRequest: String? = null, | ||
| ): SendFailureDetails { | ||
| return SendFailureDetails( | ||
| message = toSendFailureMessage(context), | ||
| failureType = toCompactFailureType(), | ||
| resetRoutingCachesOnRetry = false, | ||
| paymentRequest = paymentRequest, | ||
| ) | ||
| } | ||
|
|
||
| private fun String.snakeToLowerCamel(): String { | ||
| return lowercase() | ||
| .split("_") | ||
| .filter { it.isNotBlank() } | ||
| .mapIndexed { index, segment -> | ||
| if (index == 0) segment else segment.replaceFirstChar { it.titlecase() } | ||
| } | ||
| .joinToString("") | ||
| .ifBlank { UNKNOWN_FAILURE_TYPE } | ||
| } | ||
|
|
||
| private fun String.compactFailureType(): String { | ||
| val unwrappedOptional = removeSurrounding("Optional(", ")") | ||
| val unwrappedNodeError = unwrappedOptional.removeSurrounding("NodeError(", ")") | ||
| return unwrappedNodeError | ||
| .substringBefore("(") | ||
| .substringAfterLast(".") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Android the input is
Net effect: Minimum regression test that pins the trigger without prescribing an implementation: assertNotEquals("Unknown", Exception("Payment sending failed.").toCompactFailureType())See the test-file comment for the fuller block. Getting a real type name out of an |
||
| .trim() | ||
| .ifBlank { UNKNOWN_FAILURE_TYPE } | ||
| } | ||
|
Comment on lines
+90
to
+98
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kotlin errors here comes in a different shape, like |
||
|
|
||
| private fun String.looksInternalPaymentError(): Boolean { | ||
| return INTERNAL_PAYMENT_ERROR_MARKERS.any { contains(it, ignoreCase = true) } | ||
| } | ||
|
|
||
| private val INTERNAL_PAYMENT_ERROR_MARKERS = listOf( | ||
| "Optional(", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is also iOS exclusive error shape |
||
| "NodeError", | ||
| "DuplicatePayment", | ||
| "PaymentFailureReason", | ||
| "ldknode", | ||
| "LDK", | ||
| ) | ||
|
|
||
| private const val UNKNOWN_FAILURE_TYPE = "Unknown" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package to.bitkit.models | ||
|
|
||
| data class SendFailureDetails( | ||
| val message: String, | ||
| val failureType: String, | ||
| val resetRoutingCachesOnRetry: Boolean, | ||
| val paymentRequest: String? = null, | ||
| ) { | ||
| fun shouldResetRoutingCaches(routingCacheResetAttempted: Boolean): Boolean { | ||
| return resetRoutingCachesOnRetry && !routingCacheResetAttempted | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Android can derive the type from exception class
NodeException.DuplicatePayment -> DuplicatePayment, could use it instead of depending on the message