From bdef8337e89bf5f31e7cb76e700c6b51fadd673f Mon Sep 17 00:00:00 2001 From: rorp Date: Fri, 22 Oct 2021 00:04:29 -0700 Subject: [PATCH] Additional parameters for findroute* API calls (#1969) Add options to ignore specific channels or nodes for findRoute* APIs, and an option to specify a flat maximum fee. With these new parameters, it's now possible to do circular rebalancing of your channels. Co-authored-by: Roman Taranchenko Co-authored-by: t-bast --- docs/CircularRebalancing.md | 63 +++++++++++ docs/release-notes/eclair-vnext.md | 4 + .../main/scala/fr/acinq/eclair/Eclair.scala | 34 ++++-- .../acinq/eclair/json/JsonSerializers.scala | 25 +++-- .../api/directives/ExtraDirectives.scala | 5 +- .../eclair/api/directives/RouteFormat.scala | 32 +++--- .../eclair/api/handlers/PathFinding.scala | 21 ++-- .../acinq/eclair/api/serde/JsonSupport.scala | 3 - .../src/test/resources/api/findroute-full | 1 + .../src/test/resources/api/findroute-nodeid | 1 + .../src/test/resources/api/findroute-scid | 1 + .../fr/acinq/eclair/api/ApiServiceSpec.scala | 100 ++++++++---------- 12 files changed, 192 insertions(+), 98 deletions(-) create mode 100644 docs/CircularRebalancing.md create mode 100644 eclair-node/src/test/resources/api/findroute-full create mode 100644 eclair-node/src/test/resources/api/findroute-nodeid create mode 100644 eclair-node/src/test/resources/api/findroute-scid diff --git a/docs/CircularRebalancing.md b/docs/CircularRebalancing.md new file mode 100644 index 0000000000..591585d514 --- /dev/null +++ b/docs/CircularRebalancing.md @@ -0,0 +1,63 @@ +## How to perform circular rebalancing + +Circular rebalancing is a popular tool for managing liquidity between channels. This document describes an approach to +rebalancing using Eclair. + +In this example we assume that there are 4 participants in the Lighting Network: `Alice`, `Bob`, `Carol`, and `Diana`. + +``` + 1x1x1 + Alice --------> Bob + ^ | +4x4x4 | | 2x2x2 + | v + Diana <------- Carol + 3x3x3 +``` + +`Alice` has two channels `1x1x1` with outbound liquidity of 5M stats, and `4x4x4` with inbound liquidity of 5M sats. +Now `Alice` wants to send some sats from channel `1x1x1` to channel `4x4x4` to be able to receive and forward payments +via both channels. + +First, `Alice` creates an invoice for the desired amount (eg. 1M sats): + +```shell +eclair-cli createinvoice --description='circular rebalancing from 1x1x1 ro 4x4x4' \ + --amountMsat=1000000000 +``` + +Eclair cannot send payments to self using `payinvoice` CLI command. Fortunately, `Alice` can use `sendtoroute` CLI +command to do so. + +However, in this case `Alice` should provide a valid route from `1x1x1` to `4x4x4`. It's pretty straightforward to +build a route for our example network: `1x1x1` `->` `2x2x2` `->` `3x3x3` `->` `4x4x4`. `Alice` specifies the route using +`--shortChannelIds` parameter as a comma separated list of short channel IDs. + +```shell +eclair-cli sendtoroute --shortChannelIds=1x1x1,2x2x2,3x3x3,4x4x4 \ + --amountMsat=1000000000 \ + --invoice= +``` + +This command will send 1M sats from channel `1x1x1` to channel `4x4x4`. + +In real life its not always easy to find the most economically viable route manually, but Eclair is here to help. +Similarly to `payinvoice`, `findroute` CLI command cannot find routes to self. But `Alice` can use a little trick with +`findroutebetweennodes`, which allows finding routes between arbitrary nodes. + +To rebalance channels `1x1x1` and `4x4x4`, `Alice` wants to find a route from `Bob` (as a source node) to `Diana` (as a +target node). In our example there's at least one route from `Bob` to `Diana` via `Alice`, and in real life there can be +many more such routes, bacause `Alice` can have way more than two channels, so `Alice`'s node should be excluded from +path-finding using `--ignoreNodeIds` parameter: + +```shell +eclair-cli findroutebetweennodes --sourceNodeId= \ + --targetNodeId= \ + --ignoreNodeIds= \ + --format=shortChannelId +``` + +Then `Alice` simply appends the outgoing channel ID to the beginning of the found route and the incoming channel ID to +the end: `1x1x1,,4x4x4`. In our example the found route is `2x2x2,3x3x3`, so the full route will be +`1x1x1,2x2x2,3x3x3,4x4x4`. `Alice` can use this route with `sendtoroute` command to perform rebalancing. + diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 612850ea4d..ac537aacd6 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -51,6 +51,10 @@ Examples: This release contains many other API updates: - `deleteinvoice` allows you to remove unpaid invoices (#1984) +- `findroute`, `findroutetonode` and `findroutebetweennodes` supports new output format `full` (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreNodeIds` to specify nodes you want to be ignored in path-finding (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreShortChannelIds` to specify channels you want to be ignored in path-finding (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--maxFeeMsat` to specify an upper bound of fees (#1969) Have a look at our [API documentation](https://acinq.github.io/eclair) for more details. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index bca932dd22..61363de366 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -48,6 +48,7 @@ import scodec.bits.ByteVector import java.nio.charset.StandardCharsets import java.util.UUID +import scala.collection.immutable.SortedMap import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future, Promise} import scala.reflect.ClassTag @@ -107,9 +108,9 @@ trait Eclair { def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32] - def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse] + def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] - def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse] + def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] @@ -266,8 +267,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse] = - findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes, includeLocalChannelCost) + override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = + findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes, includeLocalChannelCost, ignoreNodeIds, ignoreShortChannelIds, maxFee_opt) private def getRouteParams(pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, RouteParams] = { pathFindingExperimentName_opt match { @@ -279,11 +280,15 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse] = { + override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = { getRouteParams(pathFindingExperimentName_opt) match { case Right(routeParams) => - val maxFee = routeParams.getMaxFee(amount) - (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] + val maxFee = maxFee_opt.getOrElse(routeParams.getMaxFee(amount)) + for { + ignoredChannels <- getChannelDescs(ignoreShortChannelIds.toSet) + ignore = Ignore(ignoreNodeIds.toSet, ignoredChannels) + response <- (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] + } yield response case Left(t) => Future.failed(t) } } @@ -475,4 +480,19 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { val pubKeyFromSignature = Crypto.recoverPublicKey(signature, signedBytes, recoveryId) VerifiedMessage(valid = true, pubKeyFromSignature) } + + private def getChannelDescs(shortChannelIds: Set[ShortChannelId])(implicit timeout: Timeout): Future[Set[ChannelDesc]] = { + if (shortChannelIds.isEmpty){ + Future.successful(Set.empty) + } else { + for { + channelsMap <- (appKit.router ? GetChannelsMap).mapTo[SortedMap[ShortChannelId, PublicChannel]] + } yield { + shortChannelIds.map { id => + val c = channelsMap.getOrElse(id, throw new IllegalArgumentException(s"unknown channel: $id")) + ChannelDesc(c.ann.shortChannelId, c.ann.nodeId1, c.ann.nodeId2) + } + } + } + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 91a316a9b4..7530630192 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.db.FailureType.FailureType import fr.acinq.eclair.db.{IncomingPaymentStatus, OutgoingPaymentStatus} import fr.acinq.eclair.payment.PaymentFailure.PaymentFailedSummary import fr.acinq.eclair.payment._ -import fr.acinq.eclair.router.Router.RouteResponse +import fr.acinq.eclair.router.Router.{ChannelHop, Route} import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ @@ -260,15 +260,23 @@ object ColorSerializer extends MinimalSerializer({ case c: Color => JString(c.toString) }) -object RouteResponseSerializer extends MinimalSerializer({ - case route: RouteResponse => - val nodeIds = route.routes.head.hops match { - case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId - case Nil => Nil - } - JArray(nodeIds.toList.map(n => JString(n.toString))) +// @formatter:off +private case class RouteFullJson(amount: MilliSatoshi, hops: Seq[ChannelHop]) +object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.hops)) + +private case class RouteNodeIdsJson(amount: MilliSatoshi, nodeIds: Seq[PublicKey]) +object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => { + val nodeIds = route.hops match { + case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId + case Nil => Nil + } + RouteNodeIdsJson(route.amount, nodeIds) }) +private case class RouteShortChannelIdsJson(amount: MilliSatoshi, shortChannelIds: Seq[ShortChannelId]) +object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.hops.map(_.lastUpdate.shortChannelId))) +// @formatter:on + // @formatter:off private case class PaymentFailureSummaryJson(amount: MilliSatoshi, route: Seq[PublicKey], message: String) private case class PaymentFailedSummaryJson(paymentHash: ByteVector32, destination: PublicKey, totalAmount: MilliSatoshi, pathFindingExperiment: String, failures: Seq[PaymentFailureSummaryJson]) @@ -487,7 +495,6 @@ object JsonSerializers { CommandResponseSerializer + InputInfoSerializer + ColorSerializer + - RouteResponseSerializer + ThrowableSerializer + FailureMessageSerializer + FailureTypeSerializer + diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index b92db29a4f..cc8e06376b 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -46,7 +46,10 @@ trait ExtraDirectives extends Directives { val toFormParam: NameDefaultUnmarshallerReceptacle[TimestampSecond] = "to".as[TimestampSecond](timestampSecondUnmarshaller).?(Long.MaxValue unixsec) val amountMsatFormParam: NameReceptacle[MilliSatoshi] = "amountMsat".as[MilliSatoshi] val invoiceFormParam: NameReceptacle[PaymentRequest] = "invoice".as[PaymentRequest] - val routeFormat: NameUnmarshallerReceptacle[RouteFormat] = "format".as[RouteFormat](routeFormatUnmarshaller) + val routeFormatFormParam: NameUnmarshallerReceptacle[RouteFormat] = "format".as[RouteFormat](routeFormatUnmarshaller) + val ignoreNodeIdsFormParam: NameUnmarshallerReceptacle[List[PublicKey]] = "ignoreNodeIds".as[List[PublicKey]](pubkeyListUnmarshaller) + val ignoreShortChannelIdsFormParam: NameUnmarshallerReceptacle[List[ShortChannelId]] = "ignoreShortChannelIds".as[List[ShortChannelId]](shortChannelIdsUnmarshaller) + val maxFeeMsatFormParam: NameReceptacle[MilliSatoshi] = "maxFeeMsat".as[MilliSatoshi] // custom directive to fail with HTTP 404 (and JSON response) if the element was not found def completeOrNotFound[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) { diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala index 60856683ca..69d54badd9 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala @@ -16,38 +16,42 @@ package fr.acinq.eclair.api.directives +import akka.http.scaladsl.model.StatusCodes.OK +import akka.http.scaladsl.model.{ContentTypes, HttpResponse} +import fr.acinq.eclair.api.serde.JsonSupport._ +import fr.acinq.eclair.json.{JsonSerializers, RouteFullSerializer, RouteNodeIdsSerializer, RouteShortChannelIdsSerializer} import fr.acinq.eclair.router.Router.RouteResponse +import org.json4s.Formats // @formatter:off sealed trait RouteFormat case object NodeIdRouteFormat extends RouteFormat case object ShortChannelIdRouteFormat extends RouteFormat +case object FullRouteFormat extends RouteFormat // @formatter:on object RouteFormat { val NODE_ID = "nodeId" val SHORT_CHANNEL_ID = "shortChannelId" + val FULL = "full" def fromString(s: String): RouteFormat = s match { case NODE_ID => NodeIdRouteFormat case SHORT_CHANNEL_ID => ShortChannelIdRouteFormat - case _ => throw new IllegalArgumentException(s"invalid route format, possible values are ($NODE_ID, $SHORT_CHANNEL_ID)") + case FULL => FullRouteFormat + case _ => throw new IllegalArgumentException(s"invalid route format, possible values are ($NODE_ID, $SHORT_CHANNEL_ID, $FULL)") } - def format(route: RouteResponse, format_opt: Option[RouteFormat]): Seq[String] = format(route, format_opt.getOrElse(NodeIdRouteFormat)) - - def format(route: RouteResponse, format: RouteFormat): Seq[String] = format match { - case NodeIdRouteFormat => - val nodeIds = route.routes.head.hops match { - case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId - case Nil => Nil - } - nodeIds.toList.map(_.toString) - case ShortChannelIdRouteFormat => - val shortChannelIds = route.routes.head.hops.map(_.lastUpdate.shortChannelId) - shortChannelIds.map(_.toString) - } + def format(route: RouteResponse, format_opt: Option[RouteFormat]): HttpResponse = format(route, format_opt.getOrElse(NodeIdRouteFormat)) + def format(route: RouteResponse, format: RouteFormat): HttpResponse = { + val serializationFormats: Formats = format match { + case NodeIdRouteFormat => JsonSerializers.formats + RouteNodeIdsSerializer + case ShortChannelIdRouteFormat => JsonSerializers.formats + RouteShortChannelIdsSerializer + case FullRouteFormat => JsonSerializers.formats + RouteFullSerializer + } + HttpResponse(OK).withEntity(ContentTypes.`application/json`, serialization.write(route)(serializationFormats)) + } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala index 1fad3a8da6..5deff727c2 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala @@ -33,11 +33,11 @@ trait PathFinding { private implicit def ec: ExecutionContext = actorSystem.dispatcher val findRoute: Route = postRequest("findroute") { implicit t => - formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormat.?, "includeLocalChannelCost".as[Boolean].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, pathFindingExperimentName_opt, routeFormat, includeLocalChannelCost_opt) => - complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false)).map(r => RouteFormat.format(r, routeFormat))) - case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat, includeLocalChannelCost_opt) => - complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false)).map(r => RouteFormat.format(r, routeFormat))) + formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => + complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => + complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) case _ => reject(MalformedFormFieldRejection( "invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'" )) @@ -45,16 +45,15 @@ trait PathFinding { } val findRouteToNode: Route = postRequest("findroutetonode") { implicit t => - formFields(nodeIdFormParam, amountMsatFormParam, "pathFindingExperimentName".?, routeFormat.?, "includeLocalChannelCost".as[Boolean].?) { - (nodeId, amount, pathFindingExperimentName_opt, routeFormat, includeLocalChannelCost_opt) => - complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false)).map(r => RouteFormat.format(r, routeFormat))) + formFields(nodeIdFormParam, amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?) { + (nodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => + complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) } } val findRouteBetweenNodes: Route = postRequest("findroutebetweennodes") { implicit t => - formFields("sourceNodeId".as[PublicKey], "targetNodeId".as[PublicKey], amountMsatFormParam, "pathFindingExperimentName".?, routeFormat.?, "includeLocalChannelCost".as[Boolean].?) { - (sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, routeFormat, includeLocalChannelCost_opt) => - complete(eclairApi.findRouteBetween(sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false)).map(r => RouteFormat.format(r, routeFormat))) + formFields("sourceNodeId".as[PublicKey], "targetNodeId".as[PublicKey], amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?) { (sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => + complete(eclairApi.findRouteBetween(sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/JsonSupport.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/JsonSupport.scala index 1930e701f2..59d2e0ee6e 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/JsonSupport.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/JsonSupport.scala @@ -21,9 +21,6 @@ import fr.acinq.eclair.json.JsonSerializers import org.json4s.{Formats, Serialization} object JsonSupport extends Json4sSupport { - implicit val serialization: Serialization = JsonSerializers.serialization - implicit val formats: Formats = JsonSerializers.formats - } diff --git a/eclair-node/src/test/resources/api/findroute-full b/eclair-node/src/test/resources/api/findroute-full new file mode 100644 index 0000000000..d41d006dd2 --- /dev/null +++ b/eclair-node/src/test/resources/api/findroute-full @@ -0,0 +1 @@ +{"routes":[{"amount":456,"hops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}}]}]} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/findroute-nodeid b/eclair-node/src/test/resources/api/findroute-nodeid new file mode 100644 index 0000000000..3671ff6df7 --- /dev/null +++ b/eclair-node/src/test/resources/api/findroute-nodeid @@ -0,0 +1 @@ +{"routes":[{"amount":456,"nodeIds":["03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df"]}]} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/findroute-scid b/eclair-node/src/test/resources/api/findroute-scid new file mode 100644 index 0000000000..68b2dc39a0 --- /dev/null +++ b/eclair-node/src/test/resources/api/findroute-scid @@ -0,0 +1 @@ +{"routes":[{"amount":456,"shortChannelIds":["1x2x3","1x2x4","1x2x5"]}]} \ No newline at end of file diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index bf707447df..b0b45709ba 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -25,7 +25,7 @@ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest, WSProbe import akka.util.Timeout import de.heikoseeberger.akkahttpjson4s.Json4sSupport import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueriesExtended, OptionDataLossProtect} @@ -46,7 +46,6 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToRouteResponse import fr.acinq.eclair.router.Router.PredefinedNodeRoute import fr.acinq.eclair.router.{NetworkStats, Router, Stats} import fr.acinq.eclair.wire.protocol.{ChannelUpdate, Color, NodeAddress} -import org.json4s.JsonAST.{JArray, JString} import org.mockito.scalatest.IdiomaticMockito import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -269,7 +268,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'open' channels with bad channelType") { val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") - val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") val eclair = mock[Eclair] val mockService = new MockService(eclair) @@ -577,7 +575,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'send' method should handle payment failures") { val eclair = mock[Eclair] - eclair.send(any, any, any, any, any, any)(any[Timeout]) returns Future.failed(new IllegalArgumentException("invoice has expired")) + eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.failed(new IllegalArgumentException("invoice has expired")) val mockService = new MockService(eclair) val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" @@ -590,7 +588,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == BadRequest) val resp = entityAs[ErrorResponse](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) assert(resp.error == "invoice has expired") - eclair.send(None, 1258000 msat, any, any, any, any)(any[Timeout]).wasCalled(once) + eclair.send(None, 1258000 msat, any, any, any, any, any)(any[Timeout]).wasCalled(once) } } @@ -599,7 +597,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val eclair = mock[Eclair] val mockService = new MockService(eclair) - eclair.sendBlocking(any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Left(PreimageReceived(ByteVector32.Zeroes, ByteVector32.One)))) + eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Left(PreimageReceived(ByteVector32.Zeroes, ByteVector32.One)))) Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> Route.seal(mockService.payInvoice) ~> @@ -613,7 +611,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val uuid = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") val paymentSent = PaymentSent(uuid, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(uuid, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L)))) - eclair.sendBlocking(any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Right(paymentSent))) + eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Right(paymentSent))) Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> Route.seal(mockService.payInvoice) ~> @@ -626,7 +624,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } val paymentFailed = PaymentFailed(uuid, ByteVector32.Zeroes, failures = Seq.empty, timestamp = TimestampMilli(1553784963659L)) - eclair.sendBlocking(any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Right(paymentFailed))) + eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Right(paymentFailed))) Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> Route.seal(mockService.payInvoice) ~> @@ -643,7 +641,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" val eclair = mock[Eclair] - eclair.send(any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) val mockService = new MockService(eclair) Post("/payinvoice", FormData("invoice" -> invoice).toEntity) ~> @@ -652,7 +650,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { assert(handled) assert(status == OK) - eclair.send(None, 1258000 msat, any, any, any, any)(any[Timeout]).wasCalled(once) + eclair.send(None, 1258000 msat, any, any, any, any, any)(any[Timeout]).wasCalled(once) } } @@ -660,7 +658,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" val eclair = mock[Eclair] - eclair.send(any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) val mockService = new MockService(eclair) Post("/payinvoice", FormData("invoice" -> invoice, "amountMsat" -> "123", "feeThresholdSat" -> "112233", "maxFeePct" -> "2.34", "externalId" -> "42").toEntity) ~> @@ -669,7 +667,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { assert(handled) assert(status == OK) - eclair.send(Some("42"), 123 msat, any, any, Some(112233 sat), Some(2.34))(any[Timeout]).wasCalled(once) + eclair.send(Some("42"), 123 msat, any, any, Some(112233 sat), Some(2.34), any)(any[Timeout]).wasCalled(once) } } @@ -677,7 +675,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" val eclair = mock[Eclair] - eclair.send(any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) val mockService = new MockService(eclair) Post("/payinvoice", FormData("invoice" -> invoice, "amountMsat" -> "456", "feeThresholdSat" -> "10", "maxFeePct" -> "0.5").toEntity) ~> @@ -686,7 +684,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { assert(handled) assert(status == OK) - eclair.send(None, 456 msat, any, any, Some(10 sat), Some(0.5))(any[Timeout]).wasCalled(once) + eclair.send(None, 456 msat, any, any, Some(10 sat), Some(0.5), any)(any[Timeout]).wasCalled(once) } } @@ -782,8 +780,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'getreceivedinfo' 1") { - val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val notFound = randomBytes32() eclair.receivedInfo(notFound)(any) returns Future.successful(None) @@ -966,13 +962,13 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } - test("'findroute' method response should support both a node ID and channel ID formats") { - val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" - + test("'findroute' method response should support nodeId, shortChannelId and full formats") { + val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" + val invoice = PaymentRequest.read(serializedInvoice) val mockChannelUpdate1 = ChannelUpdate( - signature = randomBytes64(), - chainHash = randomBytes32(), + signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), + chainHash = ByteVector32.fromValidHex("024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2"), shortChannelId = ShortChannelId(1, 2, 3), timestamp = 0 unixsec, channelFlags = ChannelUpdate.ChannelFlags.DUMMY, @@ -983,75 +979,73 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM htlcMaximumMsat = None ) - val mockHop1 = - Router.ChannelHop(nodeId = randomKey().publicKey, nextNodeId = randomKey().publicKey, mockChannelUpdate1) - val mockHop2 = - Router.ChannelHop(nodeId = mockHop1.nextNodeId, nextNodeId = randomKey().publicKey, mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 4))) - val mockHop3 = - Router.ChannelHop(nodeId = mockHop2.nextNodeId, nextNodeId = randomKey().publicKey, mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 5))) + val mockHop1 = Router.ChannelHop(PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), mockChannelUpdate1) + val mockHop2 = Router.ChannelHop(mockHop1.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 4))) + val mockHop3 = Router.ChannelHop(mockHop2.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 5))) val mockHops = Seq(mockHop1, mockHop2, mockHop3) val eclair = mock[Eclair] val mockService = new MockService(eclair) - eclair.findRoute(any, any, any, any)(any[Timeout]) returns Future.successful(Router.RouteResponse(Seq(Router.Route(456.msat, mockHops)))) + eclair.findRoute(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(Router.RouteResponse(Seq(Router.Route(456.msat, mockHops)))) // invalid format - Post("/findroute", FormData("format" -> "invalid-output-format", "invoice" -> invoice, "amountMsat" -> "456")) ~> + Post("/findroute", FormData("format" -> "invalid-output-format", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.findRoute) ~> check { assert(handled) assert(status == BadRequest) - eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any)(any[Timeout]).wasNever(called) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasNever(called) } // default format - Post("/findroute", FormData("invoice" -> invoice, "amountMsat" -> "456")) ~> + Post("/findroute", FormData("invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + addHeader("Content-Type", "application/json") ~> + Route.seal(mockService.findRoute) ~> + check { + assert(handled) + assert(status == OK) + val response = entityAs[String] + matchTestJson("findroute-nodeid", response) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) + } + + Post("/findroute", FormData("format" -> "nodeId", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.findRoute) ~> check { assert(handled) assert(status == OK) - assert(entityAs[JArray](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) == JArray(List( - JString(mockHop1.nodeId.toString()), - JString(mockHop2.nodeId.toString()), - JString(mockHop3.nodeId.toString()), - JString(mockHop3.nextNodeId.toString()) - ))) - eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any)(any[Timeout]).wasCalled(once) + val response = entityAs[String] + matchTestJson("findroute-nodeid", response) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(twice) } - Post("/findroute", FormData("format" -> "nodeId", "invoice" -> invoice, "amountMsat" -> "456")) ~> + Post("/findroute", FormData("format" -> "shortChannelId", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.findRoute) ~> check { assert(handled) assert(status == OK) - assert(entityAs[JArray](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) == JArray(List( - JString(mockHop1.nodeId.toString()), - JString(mockHop2.nodeId.toString()), - JString(mockHop3.nodeId.toString()), - JString(mockHop3.nextNodeId.toString()) - ))) - eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any)(any[Timeout]).wasCalled(twice) + val response = entityAs[String] + matchTestJson("findroute-scid", response) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(threeTimes) } - Post("/findroute", FormData("format" -> "shortChannelId", "invoice" -> invoice, "amountMsat" -> "456")) ~> + Post("/findroute", FormData("format" -> "full", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.findRoute) ~> check { assert(handled) assert(status == OK) - assert(entityAs[JArray](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) == JArray(List( - JString(mockHop1.lastUpdate.shortChannelId.toString()), - JString(mockHop2.lastUpdate.shortChannelId.toString()), - JString(mockHop3.lastUpdate.shortChannelId.toString()) - ))) - eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any)(any[Timeout]).wasCalled(threeTimes) + val response = entityAs[String] + matchTestJson("findroute-full", response) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(fourTimes) } }