From 48a68c6259313b66dd47bad638980d095a1a3464 Mon Sep 17 00:00:00 2001 From: dpad85 Date: Tue, 6 Feb 2018 15:18:33 +0100 Subject: [PATCH 1/4] Added message to every FailureMessage A FailureMessage should include more detailed input to improve feedback to the user. A `distillPaymentFailures` static method is also added to clarify the list of failures associated to a PaymentFailure. --- .../eclair/channel/ChannelExceptions.scala | 7 ++- .../eclair/payment/PaymentLifecycle.scala | 20 ++++++++ .../eclair/router/RouterExceptions.scala | 4 +- .../fr/acinq/eclair/wire/FailureMessage.scala | 49 +++++++++---------- 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 457d550885..dee43a66e5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.{BinaryData, Transaction} import fr.acinq.eclair.UInt64 -import fr.acinq.eclair.payment.Origin +import fr.acinq.eclair.payment.{Origin, Relayed} import fr.acinq.eclair.wire.{ChannelUpdate, UpdateAddHtlc} /** @@ -54,5 +54,8 @@ case class InvalidRevocation (override val channelId: BinaryDa case class CommitmentSyncError (override val channelId: BinaryData) extends ChannelException(channelId, "commitment sync error") case class RevocationSyncError (override val channelId: BinaryData) extends ChannelException(channelId, "revocation sync error") case class InvalidFailureCode (override val channelId: BinaryData) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set") -case class AddHtlcFailed (override val channelId: BinaryData, t: Throwable, origin: Origin, channelUpdate: Option[ChannelUpdate]) extends ChannelException(channelId, s"cannot add htlc with origin=$origin reason=${t.getMessage}") +case class AddHtlcFailed (override val channelId: BinaryData, t: Throwable, origin: Origin, channelUpdate: Option[ChannelUpdate]) extends ChannelException(channelId, s"cannot add htlc${origin match { + case Relayed(originChannelId, _, _, _) => s" with origin=$originChannelId" + case _ => "" +}} reason=${t.getMessage}") // @formatter:on \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index f3c104d802..c4a4dbe576 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -219,4 +219,24 @@ object PaymentLifecycle { CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream_opt = None, commit = true) -> onion.sharedSecrets } + /** + * Rewrites a list of failures to retrieve the meaningful part. + *

+ * If a list of failures with many elements ends up with a LocalFailure RouteNotFound, this RouteNotFound failure + * should be removed. This last failure is irrelevant information. In such a case only the n-1 attempts were rejected + * with a **significant reason** ; the final RouteNotFound error provides no meaningful insight. + *

+ * This method should be used by the user interface to provide a non-exhaustive but more useful feedback. + * + * @param failures a list of payment failures for a payment + */ + def distillPaymentFailures(failures: Seq[PaymentFailure]): Seq[PaymentFailure] = { + failures.lastOption match { + case Some(LocalFailure(t)) => t match { + case RouteNotFound => if (failures.size > 1) failures.dropRight(1) else failures + case _ => failures + } + case _ => failures + } + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouterExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouterExceptions.scala index 3d338c9363..737ee21b07 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouterExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouterExceptions.scala @@ -6,6 +6,6 @@ package fr.acinq.eclair.router class RouterException(message: String) extends RuntimeException(message) -object RouteNotFound extends RouterException("Route not found") +object RouteNotFound extends RouterException("route not found") -object CannotRouteToSelf extends RouterException("Cannot route to self") +object CannotRouteToSelf extends RouterException("cannot route to self") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index ea83083211..6ff9d1036b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -5,41 +5,40 @@ import fr.acinq.eclair.wire.LightningMessageCodecs.{binarydata, channelUpdateCod import scodec.Codec import scodec.codecs._ - /** * see /~https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md * Created by fabrice on 14/03/17. */ // @formatter:off -sealed trait FailureMessage +sealed trait FailureMessage { def message: String } sealed trait BadOnion extends FailureMessage { def onionHash: BinaryData } sealed trait Perm extends FailureMessage sealed trait Node extends FailureMessage sealed trait Update extends FailureMessage { def update: ChannelUpdate } -case object InvalidRealm extends Perm -case object TemporaryNodeFailure extends Node -case object PermanentNodeFailure extends Perm with Node -case object RequiredNodeFeatureMissing extends Perm with Node -case class InvalidOnionVersion(onionHash: BinaryData) extends BadOnion with Perm -case class InvalidOnionHmac(onionHash: BinaryData) extends BadOnion with Perm -case class InvalidOnionKey(onionHash: BinaryData) extends BadOnion with Perm -case class TemporaryChannelFailure(update: ChannelUpdate) extends Update -case object PermanentChannelFailure extends Perm -case object RequiredChannelFeatureMissing extends Perm -case object UnknownNextPeer extends Perm -case class AmountBelowMinimum(amountMsat: Long, update: ChannelUpdate) extends Update -case class FeeInsufficient(amountMsat: Long, update: ChannelUpdate) extends Update -case class IncorrectCltvExpiry(expiry: Long, update: ChannelUpdate) extends Update -case class ExpiryTooSoon(update: ChannelUpdate) extends Update -case class ChannelDisabled(flags: BinaryData, update: ChannelUpdate) extends Update -case object UnknownPaymentHash extends Perm -case object IncorrectPaymentAmount extends Perm -case object FinalExpiryTooSoon extends FailureMessage -case class FinalIncorrectCltvExpiry(expiry: Long) extends FailureMessage -case class FinalIncorrectHtlcAmount(amountMsat: Long) extends FailureMessage -case object ExpiryTooFar extends FailureMessage +case object InvalidRealm extends Perm { def message = "realm was not understood by the processing node" } +case object TemporaryNodeFailure extends Node { def message = "processing node was unable to handle the payment" } +case object PermanentNodeFailure extends Perm with Node { def message = "processing node is permanently unable to handle any payments" } +case object RequiredNodeFeatureMissing extends Perm with Node { def message = "processing node requires features that are missing from this onion" } +case class InvalidOnionVersion(onionHash: BinaryData) extends BadOnion with Perm { def message = "onion version was not understood by the processing node" } +case class InvalidOnionHmac(onionHash: BinaryData) extends BadOnion with Perm { def message = "onion HMAC was incorrect when it reached the processing node" } +case class InvalidOnionKey(onionHash: BinaryData) extends BadOnion with Perm { def message = "onion key was unparsable by the processing node" } +case class TemporaryChannelFailure(update: ChannelUpdate) extends Update { def message = s"channel=${java.lang.Long.toHexString(update.shortChannelId)} was unable to transport the payment" } +case object PermanentChannelFailure extends Perm { def message = "channel is permanently unable to transport any payments" } +case object RequiredChannelFeatureMissing extends Perm { def message = "channel requires features not present in the onion" } +case object UnknownNextPeer extends Perm { def message = "processing node does not know the next peer in the specified route" } +case class AmountBelowMinimum(amountMsat: Long, update: ChannelUpdate) extends Update { def message = s"payment amount was below the minimum required by the channel" } +case class FeeInsufficient(amountMsat: Long, update: ChannelUpdate) extends Update { def message = s"payment fee was below the minimum required by the channel" } +case class ChannelDisabled(flags: BinaryData, update: ChannelUpdate) extends Update { def message = "the channel has been disabled" } +case class IncorrectCltvExpiry(expiry: Long, update: ChannelUpdate) extends Update { def message = "payment expiry doesn't match the value in the onion" } +case object UnknownPaymentHash extends Perm { def message = "the payment hash is unknown to the final node" } +case object IncorrectPaymentAmount extends Perm { def message = "the amount in that payment is incorrect." } +case class ExpiryTooSoon(update: ChannelUpdate) extends Update { def message = "payment expiry is too close to the current block height for safe handling by the relaying node" } +case object FinalExpiryTooSoon extends FailureMessage { def message = "payment expiry is too close to the current block height for safe handling by the final node" } +case class FinalIncorrectCltvExpiry(expiry: Long) extends FailureMessage { def message = "payment expiry doesn't match the value in the onion" } +case class FinalIncorrectHtlcAmount(amountMsat: Long) extends FailureMessage { def message = "the amount in that payment is incorrect." } +case object ExpiryTooFar extends FailureMessage { def message = "payment expiry is too far in the future" } // @formatter:on object FailureMessageCodecs { @@ -66,7 +65,7 @@ object FailureMessageCodecs { .typecase(PERM | 10, provide(UnknownNextPeer)) .typecase(UPDATE | 11, (("amountMsat" | uint64) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[AmountBelowMinimum]) .typecase(UPDATE | 12, (("amountMsat" | uint64) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[FeeInsufficient]) - .typecase(UPDATE | 13, (("expiry" | uint32) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[IncorrectCltvExpiry]) + .typecase(UPDATE | 13, (("expiry" | uint32) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[IncorrectCltvExpiry]) .typecase(UPDATE | 14, (("channelUpdate" | channelUpdateWithLengthCodec)).as[ExpiryTooSoon]) .typecase(UPDATE | 20, (("flags" | binarydata(2)) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[ChannelDisabled]) .typecase(PERM | 15, provide(UnknownPaymentHash)) From 7a14a6e09c768ef7d70f5b6db7b6281d467c145a Mon Sep 17 00:00:00 2001 From: dpad85 Date: Tue, 6 Feb 2018 15:22:29 +0100 Subject: [PATCH 2/4] (gui) payment notification now show failure message for each attempts Payments make multiple attempts and the payment failure feedback should reflect that. --- .../src/main/resources/gui/main/main.css | 2 +- .../main/resources/gui/main/notificationPane.fxml | 4 ++-- .../main/scala/fr/acinq/eclair/gui/FxApp.scala | 2 +- .../main/scala/fr/acinq/eclair/gui/Handlers.scala | 15 ++++++++------- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/eclair-node-gui/src/main/resources/gui/main/main.css b/eclair-node-gui/src/main/resources/gui/main/main.css index 07757359fb..6a40196fea 100644 --- a/eclair-node-gui/src/main/resources/gui/main/main.css +++ b/eclair-node-gui/src/main/resources/gui/main/main.css @@ -113,7 +113,7 @@ -fx-text-fill: rgb(220, 220, 220); } .notification-pane .label.notification-message { - -fx-font-size: 16px; + -fx-font-size: 14px; -fx-font-weight: bold; } .button.notification-close { diff --git a/eclair-node-gui/src/main/resources/gui/main/notificationPane.fxml b/eclair-node-gui/src/main/resources/gui/main/notificationPane.fxml index 402f8853f4..9d3142e1c6 100644 --- a/eclair-node-gui/src/main/resources/gui/main/notificationPane.fxml +++ b/eclair-node-gui/src/main/resources/gui/main/notificationPane.fxml @@ -10,10 +10,10 @@ - + - + diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala index 687b9e8a31..df118823a1 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala @@ -134,7 +134,7 @@ class FxApp extends Application with Logging { popup.setHideOnEscape(false) popup.setAutoFix(false) val margin = 10 - val width = 300 + val width = 400 popup.setWidth(margin + width) popup.getContent.add(root) // positioning the popup @ TOP RIGHT of screen diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala index 6fd786a0ee..c6f2179f49 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala @@ -73,13 +73,14 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte val message = CoinUtils.formatAmountInUnit(MilliSatoshi(amountMsat), FxApp.getUnit, withUnit = true) notification("Payment Sent", message, NOTIFICATION_SUCCESS) case Success(PaymentFailed(_, failures)) => - val message = s"${ - failures.lastOption match { - case Some(LocalFailure(t)) => t.getMessage - case Some(RemoteFailure(_, e)) => e.failureMessage - case _ => "Unknown error" - } - } (${failures.size} attempts)" + val distilledFailures = PaymentLifecycle.distillPaymentFailures(failures) + val message = s"${distilledFailures.size} attempts:\n${ + distilledFailures.map { + case LocalFailure(t) => s"- (local) ${t.getMessage}" + case RemoteFailure(_, e) => s"- (remote) ${e.failureMessage.message}" + case _ => "- Unknown error" + }.mkString("\n") + }" notification("Payment Failed", message, NOTIFICATION_ERROR) case Failure(t) => val message = t.getMessage From 741b7f112efc808f8c48c140d136311fd65bd4e5 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 27 Feb 2018 16:35:32 +0100 Subject: [PATCH 3/4] fixup: first pass review --- .../acinq/eclair/channel/ChannelExceptions.scala | 5 +---- .../fr/acinq/eclair/payment/PaymentLifecycle.scala | 14 +++++++------- .../main/scala/fr/acinq/eclair/gui/Handlers.scala | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index dee43a66e5..628c0b7f37 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -54,8 +54,5 @@ case class InvalidRevocation (override val channelId: BinaryDa case class CommitmentSyncError (override val channelId: BinaryData) extends ChannelException(channelId, "commitment sync error") case class RevocationSyncError (override val channelId: BinaryData) extends ChannelException(channelId, "revocation sync error") case class InvalidFailureCode (override val channelId: BinaryData) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set") -case class AddHtlcFailed (override val channelId: BinaryData, t: Throwable, origin: Origin, channelUpdate: Option[ChannelUpdate]) extends ChannelException(channelId, s"cannot add htlc${origin match { - case Relayed(originChannelId, _, _, _) => s" with origin=$originChannelId" - case _ => "" -}} reason=${t.getMessage}") +case class AddHtlcFailed (override val channelId: BinaryData, t: Throwable, origin: Origin, channelUpdate: Option[ChannelUpdate]) extends ChannelException(channelId, s"cannot add htlc with origin=$origin reason=${t.getMessage}") // @formatter:on \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index c4a4dbe576..6d489595aa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -4,7 +4,7 @@ import akka.actor.{ActorRef, FSM, LoggingFSM, Props, Status} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, MilliSatoshi} import fr.acinq.eclair._ -import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Register} +import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, Register} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.{ErrorPacket, Packet} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop @@ -230,12 +230,12 @@ object PaymentLifecycle { * * @param failures a list of payment failures for a payment */ - def distillPaymentFailures(failures: Seq[PaymentFailure]): Seq[PaymentFailure] = { - failures.lastOption match { - case Some(LocalFailure(t)) => t match { - case RouteNotFound => if (failures.size > 1) failures.dropRight(1) else failures - case _ => failures - } + def transformForUser(failures: Seq[PaymentFailure]): Seq[PaymentFailure] = { + failures.map { + case LocalFailure(AddHtlcFailed(_, t, _, _)) => LocalFailure(t) // we're interested in the error which caused the add-htlc to fail + case other => other + } match { + case previousFailures :+ LocalFailure(RouteNotFound) if previousFailures.nonEmpty => previousFailures case _ => failures } } diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala index c6f2179f49..ed3c66ee9f 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala @@ -73,7 +73,7 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte val message = CoinUtils.formatAmountInUnit(MilliSatoshi(amountMsat), FxApp.getUnit, withUnit = true) notification("Payment Sent", message, NOTIFICATION_SUCCESS) case Success(PaymentFailed(_, failures)) => - val distilledFailures = PaymentLifecycle.distillPaymentFailures(failures) + val distilledFailures = PaymentLifecycle.transformForUser(failures) val message = s"${distilledFailures.size} attempts:\n${ distilledFailures.map { case LocalFailure(t) => s"- (local) ${t.getMessage}" From 4f8576b882451ac15445975b92ea86d23ca532a7 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 27 Feb 2018 17:09:30 +0100 Subject: [PATCH 4/4] rephrased a few error messages --- .../fr/acinq/eclair/wire/FailureMessage.scala | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index 6ff9d1036b..98c3eb5ff9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -18,26 +18,26 @@ sealed trait Node extends FailureMessage sealed trait Update extends FailureMessage { def update: ChannelUpdate } case object InvalidRealm extends Perm { def message = "realm was not understood by the processing node" } -case object TemporaryNodeFailure extends Node { def message = "processing node was unable to handle the payment" } -case object PermanentNodeFailure extends Perm with Node { def message = "processing node is permanently unable to handle any payments" } +case object TemporaryNodeFailure extends Node { def message = "general temporary failure of the processing node" } +case object PermanentNodeFailure extends Perm with Node { def message = "general permanent failure of the processing node" } case object RequiredNodeFeatureMissing extends Perm with Node { def message = "processing node requires features that are missing from this onion" } case class InvalidOnionVersion(onionHash: BinaryData) extends BadOnion with Perm { def message = "onion version was not understood by the processing node" } case class InvalidOnionHmac(onionHash: BinaryData) extends BadOnion with Perm { def message = "onion HMAC was incorrect when it reached the processing node" } -case class InvalidOnionKey(onionHash: BinaryData) extends BadOnion with Perm { def message = "onion key was unparsable by the processing node" } -case class TemporaryChannelFailure(update: ChannelUpdate) extends Update { def message = s"channel=${java.lang.Long.toHexString(update.shortChannelId)} was unable to transport the payment" } -case object PermanentChannelFailure extends Perm { def message = "channel is permanently unable to transport any payments" } +case class InvalidOnionKey(onionHash: BinaryData) extends BadOnion with Perm { def message = "ephemeral key was unparsable by the processing node" } +case class TemporaryChannelFailure(update: ChannelUpdate) extends Update { def message = s"channel ${update.shortChannelId.toHexString} is currently unavailable" } +case object PermanentChannelFailure extends Perm { def message = "channel is permanently unavailable" } case object RequiredChannelFeatureMissing extends Perm { def message = "channel requires features not present in the onion" } -case object UnknownNextPeer extends Perm { def message = "processing node does not know the next peer in the specified route" } +case object UnknownNextPeer extends Perm { def message = "processing node does not know the next peer in the route" } case class AmountBelowMinimum(amountMsat: Long, update: ChannelUpdate) extends Update { def message = s"payment amount was below the minimum required by the channel" } case class FeeInsufficient(amountMsat: Long, update: ChannelUpdate) extends Update { def message = s"payment fee was below the minimum required by the channel" } -case class ChannelDisabled(flags: BinaryData, update: ChannelUpdate) extends Update { def message = "the channel has been disabled" } +case class ChannelDisabled(flags: BinaryData, update: ChannelUpdate) extends Update { def message = "channel is currently disabled" } case class IncorrectCltvExpiry(expiry: Long, update: ChannelUpdate) extends Update { def message = "payment expiry doesn't match the value in the onion" } -case object UnknownPaymentHash extends Perm { def message = "the payment hash is unknown to the final node" } -case object IncorrectPaymentAmount extends Perm { def message = "the amount in that payment is incorrect." } +case object UnknownPaymentHash extends Perm { def message = "payment hash is unknown to the final node" } +case object IncorrectPaymentAmount extends Perm { def message = "payment amount is incorrect" } case class ExpiryTooSoon(update: ChannelUpdate) extends Update { def message = "payment expiry is too close to the current block height for safe handling by the relaying node" } case object FinalExpiryTooSoon extends FailureMessage { def message = "payment expiry is too close to the current block height for safe handling by the final node" } case class FinalIncorrectCltvExpiry(expiry: Long) extends FailureMessage { def message = "payment expiry doesn't match the value in the onion" } -case class FinalIncorrectHtlcAmount(amountMsat: Long) extends FailureMessage { def message = "the amount in that payment is incorrect." } +case class FinalIncorrectHtlcAmount(amountMsat: Long) extends FailureMessage { def message = "payment amount is incorrect in the final htlc" } case object ExpiryTooFar extends FailureMessage { def message = "payment expiry is too far in the future" } // @formatter:on