1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 06:35:11 +01:00

Add maxFeeMsat parameter to sendtoroute RPC call (#2626)

This ensures that routes found with `findroute*` and a max fee
are correctly ignored if we later use `sendtoroute` and the route
fee has increased.
This commit is contained in:
rorp 2023-06-07 07:01:09 -07:00 committed by GitHub
parent ef77198650
commit b084d73e96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 43 additions and 12 deletions

View file

@ -431,8 +431,8 @@ object PaymentLifecycle {
override val amount = route.fold(_.amount, _.amount)
def printRoute(): String = route match {
case Left(PredefinedChannelRoute(_, _, channels)) => channels.mkString("->")
case Left(PredefinedNodeRoute(_, nodes)) => nodes.mkString("->")
case Left(PredefinedChannelRoute(_, _, channels, _)) => channels.mkString("->")
case Left(PredefinedNodeRoute(_, nodes, _)) => nodes.mkString("->")
case Right(route) => route.printNodes()
}
}

View file

@ -50,6 +50,14 @@ object RouteCalculation {
}
def finalizeRoute(d: Data, localNodeId: PublicKey, fr: FinalizeRoute)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = {
def validateMaxRouteFee(route: Route, maxFee_opt: Option[MilliSatoshi]): Try[Route] = {
val routeFee = route.channelFee(includeLocalChannelCost = false)
maxFee_opt match {
case Some(maxFee) if maxFee < routeFee => Failure(new IllegalArgumentException(s"Route fee ($routeFee) was above the maximum allowed fee ($maxFee) for route ${route.printChannels()}"))
case _ => Success(route)
}
}
Logs.withMdc(log)(Logs.mdc(
category_opt = Some(LogCategory.PAYMENT),
parentPaymentId_opt = fr.paymentContext.map(_.parentId),
@ -61,19 +69,22 @@ object RouteCalculation {
val g = extraEdges.foldLeft(d.graphWithBalances.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) }
fr.route match {
case PredefinedNodeRoute(amount, hops) =>
case PredefinedNodeRoute(amount, hops, maxFee_opt) =>
// split into sublists [(a,b),(b,c), ...] then get the edges between each of those pairs
hops.sliding(2).map { case List(v1, v2) => g.getEdgesBetween(v1, v2) }.toList match {
case edges if edges.nonEmpty && edges.forall(_.nonEmpty) =>
// select the largest edge (using balance when available, otherwise capacity).
val selectedEdges = edges.map(es => es.maxBy(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi)))
val hops = selectedEdges.map(e => ChannelHop(getEdgeRelayScid(d, localNodeId, e), e.desc.a, e.desc.b, e.params))
ctx.sender() ! RouteResponse(Route(amount, hops, None) :: Nil)
validateMaxRouteFee(Route(amount, hops, None), maxFee_opt) match {
case Success(route) => ctx.sender() ! RouteResponse(route :: Nil)
case Failure(f) => ctx.sender() ! Status.Failure(f)
}
case _ =>
// some nodes in the supplied route aren't connected in our graph
ctx.sender() ! Status.Failure(new IllegalArgumentException("Not all the nodes in the supplied route are connected with public channels"))
}
case PredefinedChannelRoute(amount, targetNodeId, shortChannelIds) =>
case PredefinedChannelRoute(amount, targetNodeId, shortChannelIds, maxFee_opt) =>
val (end, hops) = shortChannelIds.foldLeft((localNodeId, Seq.empty[ChannelHop])) {
case ((currentNode, previousHops), shortChannelId) =>
val channelDesc_opt = d.resolve(shortChannelId) match {
@ -97,7 +108,10 @@ object RouteCalculation {
if (end != targetNodeId || hops.length != shortChannelIds.length) {
ctx.sender() ! Status.Failure(new IllegalArgumentException("The sequence of channels provided cannot be used to build a route to the target node"))
} else {
ctx.sender() ! RouteResponse(Route(amount, hops, None) :: Nil)
validateMaxRouteFee(Route(amount, hops, None), maxFee_opt) match {
case Success(route) => ctx.sender() ! RouteResponse(route :: Nil)
case Failure(f) => ctx.sender() ! Status.Failure(f)
}
}
}

View file

@ -637,12 +637,13 @@ object Router {
def isEmpty: Boolean
def amount: MilliSatoshi
def targetNodeId: PublicKey
def maxFee_opt: Option[MilliSatoshi]
}
case class PredefinedNodeRoute(amount: MilliSatoshi, nodes: Seq[PublicKey]) extends PredefinedRoute {
case class PredefinedNodeRoute(amount: MilliSatoshi, nodes: Seq[PublicKey], maxFee_opt: Option[MilliSatoshi] = None) extends PredefinedRoute {
override def isEmpty = nodes.isEmpty
override def targetNodeId: PublicKey = nodes.last
}
case class PredefinedChannelRoute(amount: MilliSatoshi, targetNodeId: PublicKey, channels: Seq[ShortChannelId]) extends PredefinedRoute {
case class PredefinedChannelRoute(amount: MilliSatoshi, targetNodeId: PublicKey, channels: Seq[ShortChannelId], maxFee_opt: Option[MilliSatoshi] = None) extends PredefinedRoute {
override def isEmpty = channels.isEmpty
}
// @formatter:on

View file

@ -892,6 +892,22 @@ class RouterSpec extends BaseRouterSpec {
}
}
test("given a pre-defined channels route properly handles provided max fee") { fixture =>
import fixture._
val sender = TestProbe()
{
val preComputedRoute = PredefinedChannelRoute(10000 msat, d, Seq(scid_ab, scid_bc, scid_cd), maxFee_opt = Some(19.msat))
sender.send(router, FinalizeRoute(preComputedRoute))
sender.expectMsgType[Status.Failure]
}
{
val preComputedRoute = PredefinedChannelRoute(10000 msat, d, Seq(scid_ab, scid_bc, scid_cd), maxFee_opt = Some(20.msat))
sender.send(router, FinalizeRoute(preComputedRoute))
sender.expectMsgType[Router.RouteResponse]
}
}
test("restore stale channel that comes back from the dead") { fixture =>
import fixture._

View file

@ -56,11 +56,11 @@ trait Payment {
val sendToRoute: Route = postRequest("sendtoroute") { implicit t =>
withRoute { hops =>
formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "externalId".?, "parentId".as[UUID].?,
"trampolineSecret".as[ByteVector32].?, "trampolineFeesMsat".as[MilliSatoshi].?, "trampolineCltvExpiry".as[Int].?) {
(amountMsat, recipientAmountMsat_opt, invoice, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt) => {
"trampolineSecret".as[ByteVector32].?, "trampolineFeesMsat".as[MilliSatoshi].?, "trampolineCltvExpiry".as[Int].?, maxFeeMsatFormParam.?) {
(amountMsat, recipientAmountMsat_opt, invoice, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, maxFee_opt) => {
val route = hops match {
case Left(shortChannelIds) => PredefinedChannelRoute(amountMsat, invoice.nodeId, shortChannelIds)
case Right(nodeIds) => PredefinedNodeRoute(amountMsat, nodeIds)
case Left(shortChannelIds) => PredefinedChannelRoute(amountMsat, invoice.nodeId, shortChannelIds, maxFee_opt)
case Right(nodeIds) => PredefinedNodeRoute(amountMsat, nodeIds, maxFee_opt)
}
complete(eclairApi.sendToRoute(
recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, route, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt.map(CltvExpiryDelta))