diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index b81810ede4..a314977224 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -261,6 +261,7 @@ eclair.relay.reserved-for-accountable = 0.0 - `findroute`, `findroutetonode` and `findroutebetweennodes` now include a `maxCltvExpiryDelta` parameter (#3234) - `findroute`, `findroutetonode` and `findroutebetweennodes` now include a `fee` field for each route in their full format response (#3283) +- `findroute`, `findroutetonode`, `findroutebetweennodes`, `payinvoice`, `sendtonode`, and `payoffer` now include a `routeAddrType` parameter (#3307) - `channel-opened` was removed from the websocket in favor of `channel-funding-created`, `channel-confirmed` and `channel-ready` (#3237 and #3256) - `networkfees` and `channelstats` are removed in favor in `relaystats` (#3245) 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 568f7d4479..430ce7b112 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -140,11 +140,11 @@ trait Eclair { def receivedPayments(from: TimestampSecond, to: TimestampSecond, paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Seq[IncomingPayment]] - def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[UUID] + def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[UUID] - def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[PaymentEvent] + def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[PaymentEvent] - def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32 = randomBytes32(), maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[UUID] + def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32 = randomBytes32(), maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[UUID] def sentInfo(id: PaymentIdentifier)(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] @@ -152,9 +152,9 @@ trait Eclair { def cpfpBumpFees(targetFeeratePerByte: FeeratePerByte, outpoints: Set[OutPoint]): Future[TxId] - def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None, maxCltvExpiryDelta_opt: Option[CltvExpiryDelta] = None)(implicit timeout: Timeout): Future[RouteResponse] + def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None, maxCltvExpiryDelta_opt: Option[CltvExpiryDelta] = None, routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[RouteResponse] - def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None, maxCltvExpiryDelta_opt: Option[CltvExpiryDelta] = None)(implicit timeout: Timeout): Future[RouteResponse] + def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None, maxCltvExpiryDelta_opt: Option[CltvExpiryDelta] = None, routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[RouteResponse] def sendToRoute(recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, route: PredefinedRoute)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] @@ -196,11 +196,11 @@ trait Eclair { def sendOnionMessage(intermediateNodes_opt: Option[Seq[PublicKey]], destination: Either[PublicKey, Sphinx.RouteBlinding.BlindedRoute], expectsReply: Boolean, userCustomContent: ByteVector)(implicit timeout: Timeout): Future[SendOnionMessageResponse] - def payOffer(offer: Offer, amount: MilliSatoshi, quantity: Long, externalId_opt: Option[String] = None, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, connectDirectly: Boolean = false)(implicit timeout: Timeout): Future[UUID] + def payOffer(offer: Offer, amount: MilliSatoshi, quantity: Long, externalId_opt: Option[String] = None, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, connectDirectly: Boolean = false, routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[UUID] - def payOfferBlocking(offer: Offer, amount: MilliSatoshi, quantity: Long, externalId_opt: Option[String] = None, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, connectDirectly: Boolean = false)(implicit timeout: Timeout): Future[PaymentEvent] + def payOfferBlocking(offer: Offer, amount: MilliSatoshi, quantity: Long, externalId_opt: Option[String] = None, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, connectDirectly: Boolean = false, routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[PaymentEvent] - def payOfferTrampoline(offer: Offer, amount: MilliSatoshi, quantity: Long, trampolineNodeId: PublicKey, externalId_opt: Option[String] = None, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, connectDirectly: Boolean = false)(implicit timeout: Timeout): Future[PaymentEvent] + def payOfferTrampoline(offer: Offer, amount: MilliSatoshi, quantity: Long, trampolineNodeId: PublicKey, externalId_opt: Option[String] = None, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, connectDirectly: Boolean = false, routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[PaymentEvent] def getOnChainMasterPubKey(account: Long): String @@ -462,8 +462,8 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan } } - override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None, maxCltvExpiryDelta_opt: Option[CltvExpiryDelta] = None)(implicit timeout: Timeout): Future[RouteResponse] = - findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, extraEdges, includeLocalChannelCost, ignoreNodeIds, ignoreShortChannelIds, maxFee_opt, maxCltvExpiryDelta_opt) + override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None, maxCltvExpiryDelta_opt: Option[CltvExpiryDelta] = None, routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[RouteResponse] = + findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, extraEdges, includeLocalChannelCost, ignoreNodeIds, ignoreShortChannelIds, maxFee_opt, maxCltvExpiryDelta_opt, routeAddrType_opt) private def getRouteParams(pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, RouteParams] = { pathFindingExperimentName_opt match { @@ -475,7 +475,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan } } - override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None, maxCltvExpiryDelta_opt: Option[CltvExpiryDelta] = None)(implicit timeout: Timeout): Future[RouteResponse] = { + override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None, maxCltvExpiryDelta_opt: Option[CltvExpiryDelta] = None, routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[RouteResponse] = { getRouteParams(pathFindingExperimentName_opt) match { case Right(routeParams) => val target = ClearRecipient(targetNodeId, Features.empty, amount, CltvExpiry(appKit.nodeParams.currentBlockHeight), ByteVector32.Zeroes, upgradeAccountability = true, extraEdges) @@ -490,7 +490,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan for { ignoredChannels <- getChannelDescs(ignoreShortChannelIds.toSet) ignore = Ignore(ignoreNodeIds.toSet, ignoredChannels) - response <- appKit.router.toTyped.ask[PaymentRouteResponse](replyTo => RouteRequest(replyTo, sourceNodeId, target, routeParams1, ignore)).flatMap { + response <- appKit.router.toTyped.ask[PaymentRouteResponse](replyTo => RouteRequest(replyTo, sourceNodeId, target, routeParams1, ignore, routeAddrType_opt = routeAddrType_opt)).flatMap { case r: RouteResponse => Future.successful(r) case PaymentRouteNotFound(error) => Future.failed(error) } @@ -513,7 +513,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan } } - private def createSendPaymentRequest(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, SendPaymentToNode] = { + private def createSendPaymentRequest(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String], routeAddrType_opt: Option[AddrType] = None): Either[IllegalArgumentException, SendPaymentToNode] = { val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts) getRouteParams(pathFindingExperimentName_opt) match { case Right(defaultRouteParams) => @@ -524,27 +524,27 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan externalId_opt match { case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters")) case _ if invoice.isExpired() => Left(new IllegalArgumentException("invoice has expired")) - case _ => Right(SendPaymentToNode(ActorRef.noSender, amount, invoice, Nil, maxAttempts, externalId_opt, routeParams = routeParams)) + case _ => Right(SendPaymentToNode(ActorRef.noSender, amount, invoice, Nil, maxAttempts, externalId_opt, routeParams = routeParams, routeAddrType_opt = routeAddrType_opt)) } case Left(t) => Left(t) } } - override def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[UUID] = { - createSendPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt) match { + override def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String], routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[UUID] = { + createSendPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, routeAddrType_opt) match { case Left(ex) => Future.failed(ex) case Right(req) => (appKit.paymentInitiator ? req).mapTo[UUID] } } - override def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[PaymentEvent] = { - createSendPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt) match { + override def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String], routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[PaymentEvent] = { + createSendPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, routeAddrType_opt) match { case Left(ex) => Future.failed(ex) case Right(req) => (appKit.paymentInitiator ? req.copy(blockUntilComplete = true)).mapTo[PaymentEvent] } } - override def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[UUID] = { + override def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String], routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[UUID] = { val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts) getRouteParams(pathFindingExperimentName_opt) match { case Right(defaultRouteParams) => @@ -552,7 +552,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan maxFeeProportional = maxFeePct_opt.map(_ / 100).getOrElse(defaultRouteParams.boundaries.maxFeeProportional), maxFeeFlat = maxFeeFlat_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.boundaries.maxFeeFlat) )) - val sendPayment = SendSpontaneousPayment(amount, recipientNodeId, paymentPreimage, maxAttempts, externalId_opt, routeParams) + val sendPayment = SendSpontaneousPayment(amount, recipientNodeId, paymentPreimage, maxAttempts, externalId_opt, routeParams, routeAddrType_opt = routeAddrType_opt) (appKit.paymentInitiator ? sendPayment).mapTo[UUID] case Left(t) => Future.failed(t) } @@ -778,7 +778,8 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String], connectDirectly: Boolean, - blocking: Boolean)(implicit timeout: Timeout): Future[Any] = { + blocking: Boolean, + routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[Any] = { if (externalId_opt.exists(_.length > externalIdMaxLength)) { return Future.failed(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters")) } @@ -790,7 +791,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan )) case Left(t) => return Future.failed(t) } - val sendPaymentConfig = OfferPayment.SendPaymentConfig(externalId_opt, connectDirectly, maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts), routeParams, blocking, trampolineNodeId_opt) + val sendPaymentConfig = OfferPayment.SendPaymentConfig(externalId_opt, connectDirectly, maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts), routeParams, blocking, trampolineNodeId_opt, routeAddrType_opt) val offerPayment = appKit.system.spawnAnonymous(OfferPayment(appKit.nodeParams, appKit.postman, appKit.router, appKit.register, appKit.paymentInitiator)) offerPayment.ask((ref: typed.ActorRef[Any]) => OfferPayment.PayOffer(ref.toClassic, offer, amount, quantity, sendPaymentConfig)).flatMap { case f: OfferPayment.Failure => Future.failed(new Exception(f.toString)) @@ -806,8 +807,9 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String], - connectDirectly: Boolean)(implicit timeout: Timeout): Future[UUID] = { - payOfferInternal(offer, amount, quantity, None, externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = false).mapTo[UUID] + connectDirectly: Boolean, + routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[UUID] = { + payOfferInternal(offer, amount, quantity, None, externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = false, routeAddrType_opt).mapTo[UUID] } override def payOfferBlocking(offer: Offer, @@ -818,8 +820,9 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String], - connectDirectly: Boolean)(implicit timeout: Timeout): Future[PaymentEvent] = { - payOfferInternal(offer, amount, quantity, None, externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = true).mapTo[PaymentEvent] + connectDirectly: Boolean, + routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[PaymentEvent] = { + payOfferInternal(offer, amount, quantity, None, externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = true, routeAddrType_opt).mapTo[PaymentEvent] } override def payOfferTrampoline(offer: Offer, @@ -831,8 +834,9 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String], - connectDirectly: Boolean)(implicit timeout: Timeout): Future[PaymentEvent] = { - payOfferInternal(offer, amount, quantity, Some(trampolineNodeId), externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = true).mapTo[PaymentEvent] + connectDirectly: Boolean, + routeAddrType_opt: Option[AddrType] = None)(implicit timeout: Timeout): Future[PaymentEvent] = { + payOfferInternal(offer, amount, quantity, Some(trampolineNodeId), externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = true, routeAddrType_opt).mapTo[PaymentEvent] } override def getDescriptors(account: Long): Descriptors = appKit.nodeParams.onChainKeyManager_opt match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index 1937346aa9..eacd9544c3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -304,7 +304,7 @@ object MultiPartPaymentLifecycle { * @param maxAttempts maximum number of retries. * @param routeParams parameters to fine-tune the routing algorithm. */ - case class SendMultiPartPayment(replyTo: ActorRef, recipient: Recipient, maxAttempts: Int, routeParams: RouteParams) { + case class SendMultiPartPayment(replyTo: ActorRef, recipient: Recipient, maxAttempts: Int, routeParams: RouteParams, routeAddrType_opt: Option[AddrType] = None) { require(recipient.totalAmount > 0.msat, "total amount must be > 0") } @@ -372,7 +372,7 @@ object MultiPartPaymentLifecycle { case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PaymentPart], pending: Set[UUID], remainingAttribution_opt: Option[ByteVector]) extends Data private def createRouteRequest(replyTo: ActorRef, nodeParams: NodeParams, routeParams: RouteParams, d: PaymentProgress, cfg: SendPaymentConfig): RouteRequest = { - RouteRequest(replyTo.toTyped, nodeParams.nodeId, d.request.recipient, routeParams, d.ignore, allowMultiPart = true, d.pending.values.toSeq, Some(cfg.paymentContext)) + RouteRequest(replyTo.toTyped, nodeParams.nodeId, d.request.recipient, routeParams, d.ignore, allowMultiPart = true, d.pending.values.toSeq, Some(cfg.paymentContext), d.request.routeAddrType_opt) } private def createChildPayment(replyTo: ActorRef, route: Route, request: SendMultiPartPayment): SendPaymentToRoute = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala index 2ed7ac0b5c..a915d87678 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.message.{OnionMessages, Postman} import fr.acinq.eclair.payment.send.BlindedPathsResolver.{Resolve, ResolvedPath} import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendTrampolinePayment} import fr.acinq.eclair.payment.{Bolt12Invoice, PaymentBlindedRoute} -import fr.acinq.eclair.router.Router.RouteParams +import fr.acinq.eclair.router.Router.{AddrType, RouteParams} import fr.acinq.eclair.wire.protocol.MessageOnion.{FinalPayload, InvoicePayload} import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.wire.protocol.{OnionMessagePayloadTlv, TlvStream} @@ -62,7 +62,8 @@ object OfferPayment { maxAttempts: Int, routeParams: RouteParams, blocking: Boolean, - trampolineNodeId_opt: Option[PublicKey] = None) + trampolineNodeId_opt: Option[PublicKey] = None, + routeAddrType_opt: Option[AddrType] = None) def apply(nodeParams: NodeParams, postman: typed.ActorRef[Postman.Command], @@ -155,7 +156,7 @@ private class OfferPayment(replyTo: ActorRef, replyTo ! UnknownShortChannelIds(scids) Behaviors.stopped case WrappedResolvedPaths(resolved) => - paymentInitiator ! SendPaymentToNode(replyTo, invoice.amount, invoice, resolved, maxAttempts = sendPaymentConfig.maxAttempts, externalId = sendPaymentConfig.externalId_opt, routeParams = sendPaymentConfig.routeParams, payerKey_opt = Some(payerKey), blockUntilComplete = sendPaymentConfig.blocking) + paymentInitiator ! SendPaymentToNode(replyTo, invoice.amount, invoice, resolved, maxAttempts = sendPaymentConfig.maxAttempts, externalId = sendPaymentConfig.externalId_opt, routeParams = sendPaymentConfig.routeParams, payerKey_opt = Some(payerKey), blockUntilComplete = sendPaymentConfig.blocking, routeAddrType_opt = sendPaymentConfig.routeAddrType_opt) Behaviors.stopped } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 041c50c1fa..01c6c9e6b9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -60,11 +60,11 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(recipient.features)) :: Nil, startedAt = TimestampMilli.now(), settledAt = TimestampMilli.now()) } else if (Features.canUseFeature(nodeParams.features.invoiceFeatures(), recipient.features, Features.BasicMultiPartPayment)) { val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg, publishPreimage = !r.blockUntilComplete) - fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, recipient, r.maxAttempts, r.routeParams) + fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, recipient, r.maxAttempts, r.routeParams, r.routeAddrType_opt) context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r))) } else { val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - fsm ! PaymentLifecycle.SendPaymentToNode(self, recipient, r.maxAttempts, r.routeParams) + fsm ! PaymentLifecycle.SendPaymentToNode(self, recipient, r.maxAttempts, r.routeParams, r.routeAddrType_opt) context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r))) } @@ -75,7 +75,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val finalExpiry = nodeParams.paymentFinalExpiry.computeFinalExpiry(nodeParams.currentBlockHeight, Channel.MIN_CLTV_EXPIRY_DELTA) val recipient = SpontaneousRecipient(r.recipientNodeId, r.recipientAmount, finalExpiry, r.paymentPreimage, r.userCustomTlvs) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - fsm ! PaymentLifecycle.SendPaymentToNode(self, recipient, r.maxAttempts, r.routeParams) + fsm ! PaymentLifecycle.SendPaymentToNode(self, recipient, r.maxAttempts, r.routeParams, r.routeAddrType_opt) context become main(pending + (paymentId -> PendingSpontaneousPayment(sender(), r))) case r: SendTrampolinePayment => @@ -125,7 +125,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn case PaymentIdentifier.PaymentUUID(paymentId) => pending.get(paymentId).map(pp => (paymentId, pp)) case PaymentIdentifier.PaymentHash(paymentHash) => pending.collectFirst { case (paymentId, pp) if pp.paymentHash == paymentHash => (paymentId, pp) } case PaymentIdentifier.OfferId(offerId) => pending.collectFirst { - case (paymentId, pp@PendingPaymentToNode(_, SendPaymentToNode(_, _, invoice: Bolt12Invoice, _, _, _, _, _, _, _))) if invoice.invoiceRequest.offer.offerId == offerId => + case (paymentId, pp@PendingPaymentToNode(_, SendPaymentToNode(_, _, invoice: Bolt12Invoice, _, _, _, _, _, _, _, _))) if invoice.invoiceRequest.offer.offerId == offerId => (paymentId, pp) } } @@ -250,7 +250,8 @@ object PaymentInitiator { routeParams: RouteParams, payerKey_opt: Option[PrivateKey] = None, userCustomTlvs: Set[GenericTlv] = Set.empty, - blockUntilComplete: Boolean = false) extends SendRequestedPayment + blockUntilComplete: Boolean = false, + routeAddrType_opt: Option[AddrType] = None) extends SendRequestedPayment /** * @param recipientAmount amount that should be received by the final recipient. @@ -269,7 +270,8 @@ object PaymentInitiator { externalId: Option[String] = None, routeParams: RouteParams, userCustomTlvs: Set[GenericTlv] = Set.empty, - recordPathFindingMetrics: Boolean = false) { + recordPathFindingMetrics: Boolean = false, + routeAddrType_opt: Option[AddrType] = None) { val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index c165a46dbb..7bf15fa9e1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -65,7 +65,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Event(request: SendPaymentToNode, WaitingForRequest) => log.debug("sending {} to {}", request.amount, request.recipient.nodeId) - router ! RouteRequest(self, nodeParams.nodeId, request.recipient, request.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(self, nodeParams.nodeId, request.recipient, request.routeParams, paymentContext = Some(cfg.paymentContext), routeAddrType_opt = request.routeAddrType_opt) if (cfg.storeInDb) { paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, cfg.paymentType, request.amount, request.recipient.totalAmount, request.recipient.nodeId, TimestampMilli.now(), cfg.invoice, cfg.payerKey_opt, OutgoingPaymentStatus.Pending)) } @@ -161,7 +161,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A data.request match { case request: SendPaymentToNode => val ignore1 = PaymentFailure.updateIgnored(failure, data.ignore) - router ! RouteRequest(self, nodeParams.nodeId, data.recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(self, nodeParams.nodeId, data.recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext), routeAddrType_opt = request.routeAddrType_opt) goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.request, data.failures :+ failure, ignore1) case _: SendPaymentToRoute => log.error("unexpected retry during SendPaymentToRoute") @@ -269,7 +269,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case request: SendPaymentToNode => - router ! RouteRequest(self, nodeParams.nodeId, recipient1, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(self, nodeParams.nodeId, recipient1, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext), routeAddrType_opt = request.routeAddrType_opt) goto(WAITING_FOR_ROUTE) using WaitingForRoute(request.copy(recipient = recipient1), failures :+ failure, ignore1) } } else { @@ -280,7 +280,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case request: SendPaymentToNode => - router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore + nodeId, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore + nodeId, paymentContext = Some(cfg.paymentContext), routeAddrType_opt = request.routeAddrType_opt) goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore + nodeId) } } @@ -294,7 +294,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case request: SendPaymentToNode => - router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext), routeAddrType_opt = request.routeAddrType_opt) goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore1) } case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) => @@ -495,7 +495,7 @@ object PaymentLifecycle { * @param maxAttempts maximum number of retries. * @param routeParams parameters to fine-tune the routing algorithm. */ - case class SendPaymentToNode(replyTo: ActorRef, recipient: Recipient, maxAttempts: Int, routeParams: RouteParams) extends SendPayment { + case class SendPaymentToNode(replyTo: ActorRef, recipient: Recipient, maxAttempts: Int, routeParams: RouteParams, routeAddrType_opt: Option[AddrType] = None) extends SendPayment { require(recipient.totalAmount > 0.msat, "amount must be > 0") override val amount: MilliSatoshi = recipient.totalAmount } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 13d98e2c8a..1d8633da30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.payment.Invoice import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge import fr.acinq.eclair.router.Router._ -import fr.acinq.eclair.wire.protocol.{ChannelUpdate, NodeAnnouncement} +import fr.acinq.eclair.wire.protocol.{ChannelUpdate, IPAddress, NodeAnnouncement, OnionAddress} import scala.annotation.tailrec import scala.collection.mutable @@ -212,10 +212,11 @@ object Graph { wr: WeightRatios[PaymentPathWeight], currentBlockHeight: BlockHeight, boundaries: PaymentPathWeight => Boolean, - includeLocalChannelCost: Boolean): Seq[WeightedPath[PaymentPathWeight]] = { + includeLocalChannelCost: Boolean, + routeAddrType_opt: Option[AddrType] = None): Seq[WeightedPath[PaymentPathWeight]] = { // find the shortest path (k = 0) val targetWeight = PaymentPathWeight(amount) - dijkstraShortestPath(g, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match { + dijkstraShortestPath(g, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost, routeAddrType_opt) match { case None => Seq.empty // if we can't even find a single path, avoid returning a Seq(Seq.empty) case Some(shortestPath) => @@ -253,7 +254,7 @@ object Graph { val alreadyExploredVertices = rootPathEdges.map(_.desc.b).toSet val rootPathWeight = pathWeight(g.balances, sourceNode, rootPathEdges, amount, currentBlockHeight, wr, includeLocalChannelCost) // find the "spur" path, a sub-path going from the spur node to the target avoiding previously found sub-paths - dijkstraShortestPath(g, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match { + dijkstraShortestPath(g, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost, routeAddrType_opt) match { case Some(spurPath) => val completePath = spurPath ++ rootPathEdges val candidatePath = WeightedPath(completePath, pathWeight(g.balances, sourceNode, completePath, amount, currentBlockHeight, wr, includeLocalChannelCost)) @@ -305,7 +306,8 @@ object Graph { nodeFeatures: Features[NodeFeature], currentBlockHeight: BlockHeight, wr: WeightRatios[RichWeight], - includeLocalChannelCost: Boolean): Option[Seq[GraphEdge]] = { + includeLocalChannelCost: Boolean, + routeAddrType_opt: Option[AddrType] = None): Option[Seq[GraphEdge]] = { // the graph does not contain source/destination nodes val sourceNotInGraph = !g.graph.containsVertex(sourceNode) && !extraEdges.exists(_.desc.a == sourceNode) val targetNotInGraph = !g.graph.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode) @@ -344,7 +346,8 @@ object Graph { if (current.weight.canUseEdge(edge) && !ignoredEdges.contains(edge.desc) && !ignoredVertices.contains(neighbor) && - (neighbor == sourceNode || g.graph.getVertexFeatures(neighbor).areSupported(nodeFeatures))) { + (neighbor == sourceNode || g.graph.getVertexFeatures(neighbor).areSupported(nodeFeatures)) && + (neighbor == sourceNode || neighbor == targetNode || routeAddrType_opt.forall(t => g.graph.getVertexRouteAddrType(neighbor).forall(AddrType.isCompatible(t, _))))) { // NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that // will be relayed through that edge is the one in `currentWeight`. val neighborWeight = wr.addEdgeWeight(sourceNode, edge, g.balances.get(edge), current.weight, currentBlockHeight, includeLocalChannelCost) @@ -530,7 +533,7 @@ object Graph { } } - case class Vertex(features: Features[NodeFeature], incomingEdges: Map[ChannelDesc, GraphEdge]) { + case class Vertex(features: Features[NodeFeature], incomingEdges: Map[ChannelDesc, GraphEdge], routeAddrType_opt: Option[AddrType] = None) { def update(desc: ChannelDesc, newShortChannelId: RealShortChannelId, newCapacity: Satoshi): Vertex = incomingEdges.get(desc) match { case None => this @@ -638,6 +641,8 @@ object Graph { def getVertexFeatures(key: PublicKey): Features[NodeFeature] = vertices.get(key).map(_.features).getOrElse(Features.empty) + def getVertexRouteAddrType(key: PublicKey): Option[AddrType] = vertices.get(key).flatMap(_.routeAddrType_opt) + /** * Removes a vertex and all its associated edges (both incoming and outgoing) */ @@ -651,9 +656,17 @@ object Graph { * Or update the node features if the vertex is already present. */ def addOrUpdateVertex(ann: NodeAnnouncement): DirectedGraph = { + val hasClearnet = ann.addresses.exists(_.isInstanceOf[IPAddress]) + val hasTor = ann.addresses.exists(_.isInstanceOf[OnionAddress]) + val routeAddrType_opt = (hasClearnet, hasTor) match { + case (true, false) => Some(AddrType.Clearnet) + case (true, true) => Some(AddrType.Hybrid) + case (false, true) => Some(AddrType.Tor) + case (false, false) => None + } DirectedGraph(vertices.updatedWith(ann.nodeId) { - case Some(vertex) => Some(vertex.copy(features = ann.features.nodeAnnouncementFeatures())) - case None => Some(Vertex(ann.features.nodeAnnouncementFeatures(), Map.empty)) + case Some(vertex) => Some(vertex.copy(features = ann.features.nodeAnnouncementFeatures(), routeAddrType_opt = routeAddrType_opt)) + case None => Some(Vertex(ann.features.nodeAnnouncementFeatures(), Map.empty, routeAddrType_opt)) }) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index cba5f42684..9e93b078f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -198,9 +198,9 @@ object RouteCalculation { val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(amountToSend)) KamonExt.time(Metrics.FindRouteDuration.withTags(tags.withTag(Tags.NumberOfRoutes, routesToFind.toLong))) { val result = if (r.allowMultiPart) { - findMultiPartRoute(d.graphWithBalances, r.source, targetNodeId, amountToSend, maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, r.routeParams, currentBlockHeight) + findMultiPartRoute(d.graphWithBalances, r.source, targetNodeId, amountToSend, maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, r.routeParams, currentBlockHeight, r.routeAddrType_opt) } else { - findRoute(d.graphWithBalances, r.source, targetNodeId, amountToSend, maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, r.routeParams, currentBlockHeight) + findRoute(d.graphWithBalances, r.source, targetNodeId, amountToSend, maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, r.routeParams, currentBlockHeight, r.routeAddrType_opt) } result.map(routes => addFinalHop(r.target, routes)) match { case Success(routes) => @@ -310,8 +310,9 @@ object RouteCalculation { ignoredEdges: Set[ChannelDesc] = Set.empty, ignoredVertices: Set[PublicKey] = Set.empty, routeParams: RouteParams, - currentBlockHeight: BlockHeight): Try[Seq[Route]] = Try { - findRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams, currentBlockHeight) match { + currentBlockHeight: BlockHeight, + routeAddrType_opt: Option[AddrType] = None): Try[Seq[Route]] = Try { + findRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams, currentBlockHeight, routeAddrType_opt) match { case Right(routes) => routes.map(route => Route(amount, route.path.map(graphEdgeToHop), None)) case Left(ex) => return Failure(ex) } @@ -328,7 +329,8 @@ object RouteCalculation { ignoredEdges: Set[ChannelDesc] = Set.empty, ignoredVertices: Set[PublicKey] = Set.empty, routeParams: RouteParams, - currentBlockHeight: BlockHeight): Either[RouterException, Seq[WeightedPath[PaymentPathWeight]]] = { + currentBlockHeight: BlockHeight, + routeAddrType_opt: Option[AddrType] = None): Either[RouterException, Seq[WeightedPath[PaymentPathWeight]]] = { require(amount > 0.msat, "route amount must be strictly positive") if (localNodeId == targetNodeId) return Left(CannotRouteToSelf) @@ -341,7 +343,7 @@ object RouteCalculation { val boundaries: PaymentPathWeight => Boolean = { weight => feeOk(weight.amount - amount) && lengthOk(weight.length) && cltvOk(weight.cltv) } - val foundRoutes: Seq[WeightedPath[PaymentPathWeight]] = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amount, ignoredEdges, ignoredVertices, extraEdges, numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost) + val foundRoutes: Seq[WeightedPath[PaymentPathWeight]] = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amount, ignoredEdges, ignoredVertices, extraEdges, numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost, routeAddrType_opt) if (foundRoutes.nonEmpty) { val (directRoutes, indirectRoutes) = foundRoutes.partition(_.path.length == 1) val routes = if (routeParams.randomize) { @@ -358,7 +360,7 @@ object RouteCalculation { maxCltv = DEFAULT_ROUTE_MAX_CLTV, ) ) - findRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, relaxedRouteParams, currentBlockHeight) + findRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, relaxedRouteParams, currentBlockHeight, routeAddrType_opt) } else { Left(RouteNotFound) } @@ -389,8 +391,9 @@ object RouteCalculation { ignoredVertices: Set[PublicKey] = Set.empty, pendingHtlcs: Seq[Route] = Nil, routeParams: RouteParams, - currentBlockHeight: BlockHeight): Try[Seq[Route]] = Try { - findMultiPartRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams, currentBlockHeight) match { + currentBlockHeight: BlockHeight, + routeAddrType_opt: Option[AddrType] = None): Try[Seq[Route]] = Try { + findMultiPartRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams, currentBlockHeight, routeAddrType_opt = routeAddrType_opt) match { case Right(routes) => routes case Left(ex) => return Failure(ex) } @@ -407,7 +410,8 @@ object RouteCalculation { pendingHtlcs: Seq[Route] = Nil, routeParams: RouteParams, currentBlockHeight: BlockHeight, - now: TimestampSecond = TimestampSecond.now()): Either[RouterException, Seq[Route]] = { + now: TimestampSecond = TimestampSecond.now(), + routeAddrType_opt: Option[AddrType] = None): Either[RouterException, Seq[Route]] = { // We use Yen's k-shortest paths to find many paths for chunks of the total amount. // When the recipient is a direct peer, we have complete visibility on our local channels so we can use more accurate MPP parameters. val routeParams1 = { @@ -425,7 +429,7 @@ object RouteCalculation { val minPartAmount = routeParams.mpp.minPartAmount.max(amount / numRoutes).min(amount) routeParams.copy(mpp = MultiPartParams(minPartAmount, numRoutes, routeParams.mpp.splittingStrategy)) } - findRouteInternal(g, localNodeId, targetNodeId, routeParams1.mpp.minPartAmount, maxFee, routeParams1.mpp.maxParts, extraEdges, ignoredEdges, ignoredVertices, routeParams1, currentBlockHeight) match { + findRouteInternal(g, localNodeId, targetNodeId, routeParams1.mpp.minPartAmount, maxFee, routeParams1.mpp.maxParts, extraEdges, ignoredEdges, ignoredVertices, routeParams1, currentBlockHeight, routeAddrType_opt) match { case Right(paths) => // We use these shortest paths to find a set of non-conflicting HTLCs that send the total amount. split(amount, mutable.Queue(paths: _*), initializeUsedCapacity(pendingHtlcs), routeParams1, g.balances, now) match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index ad1a48d359..908cb2930b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -628,6 +628,22 @@ object Router { case class MessageRouteParams(maxRouteLength: Int, ratios: MessageWeightRatios) + // @formatter:off + sealed trait AddrType + object AddrType { + case object Clearnet extends AddrType + case object Hybrid extends AddrType + case object Tor extends AddrType + + /** Returns true if a node with the given address type is eligible when routing with the requested filter. */ + def isCompatible(filter: AddrType, nodeAddrType: AddrType): Boolean = filter match { + case Clearnet => nodeAddrType == Clearnet + case Hybrid => nodeAddrType == Clearnet || nodeAddrType == Hybrid + case Tor => nodeAddrType == Tor + } + } + // @formatter:on + case class Ignore(nodes: Set[PublicKey], channels: Set[ChannelDesc]) { // @formatter:off def +(ignoreNode: PublicKey): Ignore = copy(nodes = nodes + ignoreNode) @@ -648,7 +664,8 @@ object Router { ignore: Ignore = Ignore.empty, allowMultiPart: Boolean = false, pendingPayments: Seq[Route] = Nil, - paymentContext: Option[PaymentContext] = None) + paymentContext: Option[PaymentContext] = None, + routeAddrType_opt: Option[AddrType] = None) case class BlindedRouteRequest(replyTo: typed.ActorRef[PaymentRouteResponse], source: PublicKey, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index cb901c8cf5..11c53cbaec 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -1940,6 +1940,150 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(route2 :: Nil) = findRoute(h, a, e, amount, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = hc, includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route2) == 1 :: 4 :: 5 :: Nil) } + + test("route addr type filter") { + // Topology: three parallel two-hop paths a -> {b,c,e} -> f + // b = clearnet-only (edges 1,2) + // c = hybrid (edges 3,4) — cheaper than b + // e = tor-only (edges 5,6) — cheapest + // source (a) and target (f) have no addresses and are always allowed. + val privB = randomKey() + val privC = randomKey() + val privE = randomKey() + val privF = randomKey() + + val nodeB = privB.publicKey + val nodeC = privC.publicKey + val nodeE = privE.publicKey + val nodeF = privF.publicKey + + val clearnetAddr = NodeAddress.fromParts("140.82.121.4", 9735).get + val torAddr = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get + + val annB = makeNodeAnnouncement(privB, "B", Color(0, 0, 0), List(clearnetAddr), Features.empty) + val annC = makeNodeAnnouncement(privC, "C", Color(0, 0, 0), List(clearnetAddr, torAddr), Features.empty) + val annE = makeNodeAnnouncement(privE, "E", Color(0, 0, 0), List(torAddr), Features.empty) + + val g = GraphWithBalanceEstimates(DirectedGraph(List( + makeEdge(1L, a, nodeB, 3 msat, 0), // a -> b (clearnet path, expensive) + makeEdge(2L, nodeB, nodeF, 3 msat, 0), + makeEdge(3L, a, nodeC, 2 msat, 0), // a -> c (hybrid path, medium) + makeEdge(4L, nodeC, nodeF, 2 msat, 0), + makeEdge(5L, a, nodeE, 1 msat, 0), // a -> e (tor path, cheapest) + makeEdge(6L, nodeE, nodeF, 1 msat, 0), + )).addOrUpdateVertex(annB).addOrUpdateVertex(annC).addOrUpdateVertex(annE), 1 day) + + // clearnet: only the clearnet-only node b is eligible + val Success(routeClearnet :: Nil) = findRoute(g, a, nodeF, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000), routeAddrType_opt = Some(AddrType.Clearnet)) + assert(route2Ids(routeClearnet) == 1L :: 2L :: Nil) + + // hybrid: clearnet and hybrid nodes (b and c) are eligible; c is cheaper so it wins + val Success(routeHybrid :: Nil) = findRoute(g, a, nodeF, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000), routeAddrType_opt = Some(AddrType.Hybrid)) + assert(route2Ids(routeHybrid) == 3L :: 4L :: Nil) + + // tor: only the tor-only node e is eligible + val Success(routeTor :: Nil) = findRoute(g, a, nodeF, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000), routeAddrType_opt = Some(AddrType.Tor)) + assert(route2Ids(routeTor) == 5L :: 6L :: Nil) + + // clearnet filter blocks hybrid and tor nodes: only b available; c and e must not appear in the route + assert(!route2Ids(routeClearnet).contains(3L) && !route2Ids(routeClearnet).contains(4L)) + assert(!route2Ids(routeClearnet).contains(5L) && !route2Ids(routeClearnet).contains(6L)) + + // tor filter blocks clearnet and hybrid nodes: b and c must not appear + assert(!route2Ids(routeTor).contains(1L) && !route2Ids(routeTor).contains(2L)) + assert(!route2Ids(routeTor).contains(3L) && !route2Ids(routeTor).contains(4L)) + } + + test("route addr type filter: no eligible intermediate nodes") { + val privB = randomKey() + val privF = randomKey() + + val nodeB = privB.publicKey + val nodeF = privF.publicKey + + val torAddr = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get + val annB = makeNodeAnnouncement(privB, "B", Color(0, 0, 0), List(torAddr), Features.empty) + + val g = GraphWithBalanceEstimates(DirectedGraph(List( + makeEdge(1L, a, nodeB, 1 msat, 0), + makeEdge(2L, nodeB, nodeF, 1 msat, 0), + )).addOrUpdateVertex(annB), 1 day) + + // tor-only node b is not eligible under clearnet filter → no route + assert(findRoute(g, a, nodeF, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000), routeAddrType_opt = Some(AddrType.Clearnet)) == Failure(RouteNotFound)) + // tor-only node b is not eligible under hybrid filter → no route + assert(findRoute(g, a, nodeF, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000), routeAddrType_opt = Some(AddrType.Hybrid)) == Failure(RouteNotFound)) + // tor-only node b is eligible under tor filter → route found + val Success(_ :: Nil) = findRoute(g, a, nodeF, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000), routeAddrType_opt = Some(AddrType.Tor)) + } + + test("route addr type filter: hybrid selects both clearnet-only and hybrid nodes") { + val privB = randomKey() + val privC = randomKey() + val privE = randomKey() + val privF = randomKey() + + val nodeB = privB.publicKey // clearnet-only + val nodeC = privC.publicKey // hybrid + val nodeE = privE.publicKey // tor-only + val nodeF = privF.publicKey + + val clearnetAddr = NodeAddress.fromParts("140.82.121.4", 9735).get + val torAddr = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get + + val annB = makeNodeAnnouncement(privB, "B", Color(0, 0, 0), List(clearnetAddr), Features.empty) + val annC = makeNodeAnnouncement(privC, "C", Color(0, 0, 0), List(clearnetAddr, torAddr), Features.empty) + val annE = makeNodeAnnouncement(privE, "E", Color(0, 0, 0), List(torAddr), Features.empty) + + val g = GraphWithBalanceEstimates(DirectedGraph(List( + makeEdge(1L, a, nodeB, 1 msat, 0), // path through clearnet-only node + makeEdge(2L, nodeB, nodeF, 1 msat, 0), + makeEdge(3L, a, nodeC, 1 msat, 0), // path through hybrid node + makeEdge(4L, nodeC, nodeF, 1 msat, 0), + makeEdge(5L, a, nodeE, 1 msat, 0), // path through tor-only node + makeEdge(6L, nodeE, nodeF, 1 msat, 0), + )).addOrUpdateVertex(annB).addOrUpdateVertex(annC).addOrUpdateVertex(annE), 1 day) + + val Success(routes) = findRoute(g, a, nodeF, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000), routeAddrType_opt = Some(AddrType.Hybrid)) + val allEdgeIds = routes.flatMap(route2Ids).toSet + assert(allEdgeIds.contains(1L) && allEdgeIds.contains(2L), "clearnet-only path should be included") + assert(allEdgeIds.contains(3L) && allEdgeIds.contains(4L), "hybrid path should be included") + assert(!allEdgeIds.contains(5L) && !allEdgeIds.contains(6L), "tor-only path should be excluded") + } + + test("route addr type filter: None allows all types") { + val privB = randomKey() + val privC = randomKey() + val privE = randomKey() + val privF = randomKey() + + val nodeB = privB.publicKey // clearnet-only + val nodeC = privC.publicKey // hybrid + val nodeE = privE.publicKey // tor-only + val nodeF = privF.publicKey + + val clearnetAddr = NodeAddress.fromParts("140.82.121.4", 9735).get + val torAddr = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get + + val annB = makeNodeAnnouncement(privB, "B", Color(0, 0, 0), List(clearnetAddr), Features.empty) + val annC = makeNodeAnnouncement(privC, "C", Color(0, 0, 0), List(clearnetAddr, torAddr), Features.empty) + val annE = makeNodeAnnouncement(privE, "E", Color(0, 0, 0), List(torAddr), Features.empty) + + val g = GraphWithBalanceEstimates(DirectedGraph(List( + makeEdge(1L, a, nodeB, 1 msat, 0), + makeEdge(2L, nodeB, nodeF, 1 msat, 0), + makeEdge(3L, a, nodeC, 1 msat, 0), + makeEdge(4L, nodeC, nodeF, 1 msat, 0), + makeEdge(5L, a, nodeE, 1 msat, 0), + makeEdge(6L, nodeE, nodeF, 1 msat, 0), + )).addOrUpdateVertex(annB).addOrUpdateVertex(annC).addOrUpdateVertex(annE), 1 day) + + val Success(routes) = findRoute(g, a, nodeF, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000), routeAddrType_opt = None) + val allEdgeIds = routes.flatMap(route2Ids).toSet + assert(allEdgeIds.contains(1L) && allEdgeIds.contains(2L), "clearnet-only path should be included") + assert(allEdgeIds.contains(3L) && allEdgeIds.contains(4L), "hybrid path should be included") + assert(allEdgeIds.contains(5L) && allEdgeIds.contains(6L), "tor-only path should be included") + } } object RouteCalculationSpec { 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 bf18a841a1..2485f2fb25 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 @@ -28,6 +28,7 @@ import fr.acinq.eclair.api.serde.FormParamExtractors._ import fr.acinq.eclair.api.serde.JsonSupport._ import fr.acinq.eclair.blockchain.fee.ConfirmationPriority import fr.acinq.eclair.payment.Bolt11Invoice +import fr.acinq.eclair.router.Router.AddrType import fr.acinq.eclair.wire.protocol.OfferTypes.Offer import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, Paginated, ShortChannelId, TimestampSecond} @@ -57,6 +58,7 @@ trait ExtraDirectives extends Directives { val skipFormParam: NameReceptacle[Int] = "skip".as[Int] val offerFormParam: NameUnmarshallerReceptacle[Offer] = "offer".as[Offer](offerUnmarshaller) val confirmationPriorityFormParam: NameUnmarshallerReceptacle[ConfirmationPriority] = "priority".as[ConfirmationPriority](confirmationPriorityUnmarshaller) + val routeAddrTypeFormParam: NameUnmarshallerReceptacle[AddrType] = "routeAddrType".as[AddrType](addrTypeUnmarshaller) // @formatter:off // We limit default values to avoid accidentally reading too much data from the DB. 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 2f6ae89efd..45731d12a3 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 @@ -32,27 +32,27 @@ trait PathFinding { private implicit def ec: ExecutionContext = actorSystem.dispatcher val findRoute: Route = postRequest("findroute") { implicit t => - formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?, maxCltvExpiryDeltaFormParam.?) { - case (invoice, None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt, maxCltv_opt) if invoice.amount_opt.nonEmpty => - complete(eclairApi.findRoute(invoice.nodeId, invoice.amount_opt.get, pathFindingExperimentName_opt, invoice.extraEdges, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt, maxCltvExpiryDelta_opt = maxCltv_opt).map(r => RouteFormat.format(r, routeFormat_opt))) - case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt, maxCltv_opt) => - complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.extraEdges, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt, maxCltvExpiryDelta_opt = maxCltv_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?, maxCltvExpiryDeltaFormParam.?, routeAddrTypeFormParam.?) { + case (invoice, None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt, maxCltv_opt, routeAddrType_opt) if invoice.amount_opt.nonEmpty => + complete(eclairApi.findRoute(invoice.nodeId, invoice.amount_opt.get, pathFindingExperimentName_opt, invoice.extraEdges, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt, maxCltvExpiryDelta_opt = maxCltv_opt, routeAddrType_opt = routeAddrType_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt, maxCltv_opt, routeAddrType_opt) => + complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.extraEdges, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt, maxCltvExpiryDelta_opt = maxCltv_opt, routeAddrType_opt = routeAddrType_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'")) } } val findRouteToNode: Route = postRequest("findroutetonode") { implicit t => - formFields(nodeIdFormParam, amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?, maxCltvExpiryDeltaFormParam.?) { - (nodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt, maxCltv_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, maxCltvExpiryDelta_opt = maxCltv_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + formFields(nodeIdFormParam, amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?, maxCltvExpiryDeltaFormParam.?, routeAddrTypeFormParam.?) { + (nodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt, maxCltv_opt, routeAddrType_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, maxCltvExpiryDelta_opt = maxCltv_opt, routeAddrType_opt = routeAddrType_opt).map(r => RouteFormat.format(r, routeFormat_opt))) } } val findRouteBetweenNodes: Route = postRequest("findroutebetweennodes") { implicit t => - formFields("sourceNodeId".as[PublicKey], "targetNodeId".as[PublicKey], amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?, maxCltvExpiryDeltaFormParam.?) { - (sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt, maxCltv_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, maxCltvExpiryDelta_opt = maxCltv_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + formFields("sourceNodeId".as[PublicKey], "targetNodeId".as[PublicKey], amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?, maxCltvExpiryDeltaFormParam.?, routeAddrTypeFormParam.?) { + (sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt, maxCltv_opt, routeAddrType_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, maxCltvExpiryDelta_opt = maxCltv_opt, routeAddrType_opt = routeAddrType_opt).map(r => RouteFormat.format(r, routeFormat_opt))) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala index 44f1669a17..470b1ef8d5 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala @@ -38,16 +38,16 @@ trait Payment { } val payInvoice: Route = postRequest("payinvoice") { implicit t => - formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "blocking".as[Boolean].?, "pathFindingExperimentName".?) { - case (invoice@Bolt11Invoice(_, Some(amount), _, _, _, _), None, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, blocking_opt, pathFindingExperimentName_opt) => + formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "blocking".as[Boolean].?, "pathFindingExperimentName".?, routeAddrTypeFormParam.?) { + case (invoice@Bolt11Invoice(_, Some(amount), _, _, _, _), None, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, blocking_opt, pathFindingExperimentName_opt, routeAddrType_opt) => blocking_opt match { - case Some(true) => complete(eclairApi.sendBlocking(externalId_opt, amount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) - case _ => complete(eclairApi.send(externalId_opt, amount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) + case Some(true) => complete(eclairApi.sendBlocking(externalId_opt, amount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, routeAddrType_opt)) + case _ => complete(eclairApi.send(externalId_opt, amount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, routeAddrType_opt)) } - case (invoice, Some(overrideAmount), maxAttempts, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, blocking_opt, pathFindingExperimentName_opt) => + case (invoice, Some(overrideAmount), maxAttempts, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, blocking_opt, pathFindingExperimentName_opt, routeAddrType_opt) => blocking_opt match { - case Some(true) => complete(eclairApi.sendBlocking(externalId_opt, overrideAmount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) - case _ => complete(eclairApi.send(externalId_opt, overrideAmount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) + case Some(true) => complete(eclairApi.sendBlocking(externalId_opt, overrideAmount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, routeAddrType_opt)) + case _ => complete(eclairApi.send(externalId_opt, overrideAmount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, routeAddrType_opt)) } case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) } @@ -68,9 +68,9 @@ trait Payment { } val sendToNode: Route = postRequest("sendtonode") { implicit t => - formFields(amountMsatFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "pathFindingExperimentName".?) { - case (amountMsat, nodeId, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, pathFindingExperimentName_opt) => - complete(eclairApi.sendWithPreimage(externalId_opt, nodeId, amountMsat, randomBytes32(), maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) + formFields(amountMsatFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "pathFindingExperimentName".?, routeAddrTypeFormParam.?) { + case (amountMsat, nodeId, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, pathFindingExperimentName_opt, routeAddrType_opt) => + complete(eclairApi.sendWithPreimage(externalId_opt, nodeId, amountMsat, randomBytes32(), maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, routeAddrType_opt)) } } @@ -101,11 +101,11 @@ trait Payment { } val payOffer: Route = postRequest("payoffer") { implicit t => - formFields(offerFormParam, amountMsatFormParam, "quantity".as[Long].?, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "pathFindingExperimentName".?, "connectDirectly".as[Boolean].?, "blocking".as[Boolean].?) { - case (offer, amountMsat, quantity_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, pathFindingExperimentName_opt, connectDirectly, blocking_opt) => + formFields(offerFormParam, amountMsatFormParam, "quantity".as[Long].?, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "pathFindingExperimentName".?, "connectDirectly".as[Boolean].?, "blocking".as[Boolean].?, routeAddrTypeFormParam.?) { + case (offer, amountMsat, quantity_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, pathFindingExperimentName_opt, connectDirectly, blocking_opt, routeAddrType_opt) => blocking_opt match { - case Some(true) => complete(eclairApi.payOfferBlocking(offer, amountMsat, quantity_opt.getOrElse(1), externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly.getOrElse(false))) - case _ => complete(eclairApi.payOffer(offer, amountMsat, quantity_opt.getOrElse(1), externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly.getOrElse(false))) + case Some(true) => complete(eclairApi.payOfferBlocking(offer, amountMsat, quantity_opt.getOrElse(1), externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly.getOrElse(false), routeAddrType_opt)) + case _ => complete(eclairApi.payOffer(offer, amountMsat, quantity_opt.getOrElse(1), externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly.getOrElse(false), routeAddrType_opt)) } } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index 52f148953a..21b3546160 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -29,6 +29,7 @@ import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.wire.protocol.OfferCodecs.blindedRouteCodec import fr.acinq.eclair.wire.protocol.OfferTypes.Offer +import fr.acinq.eclair.router.Router.AddrType import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector @@ -98,6 +99,13 @@ object FormParamExtractors { case priority => throw new IllegalArgumentException(s"unknown confirmation priority '$priority'") } + val addrTypeUnmarshaller: Unmarshaller[String, AddrType] = Unmarshaller.strict { + case "clearnet" => AddrType.Clearnet + case "hybrid" => AddrType.Hybrid + case "tor" => AddrType.Tor + case addrType => throw new IllegalArgumentException(s"unknown address type '$addrType', expected: clearnet, hybrid, tor") + } + private def listUnmarshaller[T](unmarshal: String => T): Unmarshaller[String, List[T]] = Unmarshaller.strict { str => Try(serialization.read[List[String]](str).map(unmarshal)) .recoverWith(_ => Try(str.split(",").toList.map(unmarshal))) 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 f3205591f3..6950a41953 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 @@ -600,7 +600,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)(any[Timeout]) returns Future.failed(new IllegalArgumentException("invoice has expired")) + eclair.send(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.failed(new IllegalArgumentException("invoice has expired")) val mockService = new MockService(eclair) val invoice = "lnbc12580n1pw2ywztsp5mx6av0wv087hew596jz8d7egzjdt4n8auwwj8yqxl3jemj050uxqpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqq8w9xy4" @@ -613,7 +613,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)(any[Timeout]).wasCalled(once) + eclair.send(None, 1258000 msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) } } @@ -625,7 +625,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val uuid = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") val paymentSent = PaymentSent(uuid, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PaymentPart(uuid, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, TimestampMilli(1553784337711L)), 3 msat, None, TimestampMilli(1553784337650L))), None, TimestampMilli(1553784337120L)) - eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(paymentSent)) + eclair.sendBlocking(any, any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(paymentSent)) Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> Route.seal(mockService.payInvoice) ~> @@ -638,7 +638,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } val paymentFailed = PaymentFailed(uuid, ByteVector32.Zeroes, failures = Seq.empty, startedAt = TimestampMilli(1553784963507L), settledAt = TimestampMilli(1553784963659L)) - eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(paymentFailed)) + eclair.sendBlocking(any, any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(paymentFailed)) Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> Route.seal(mockService.payInvoice) ~> @@ -655,7 +655,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val invoice = "lnbc12580n1pw2ywztsp5h2xzrq2xceh0vtx083t557vlzlxt0e72t80maqkxfm6l05pqvpxqpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqyd9qst" val eclair = mock[Eclair] - eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.send(any, 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) ~> @@ -664,7 +664,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)(any[Timeout]).wasCalled(once) + eclair.send(None, 1258000 msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) } } @@ -672,7 +672,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val invoice = "lnbc12580n1pw2ywztsp5nyxta4fpym3q0fqx3f7l70hvlwu3murlm4y46tcyr7g8k4kz0stqpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqkj59dt" val eclair = mock[Eclair] - eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.send(any, 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", "maxFeeFlatSat" -> "112233", "maxFeePct" -> "2.34", "externalId" -> "42").toEntity) ~> @@ -681,7 +681,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)(any[Timeout]).wasCalled(once) + eclair.send(Some("42"), 123 msat, any, any, Some(112233 sat), Some(2.34), any, any)(any[Timeout]).wasCalled(once) } } @@ -689,7 +689,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val invoice = "lnbc12580n1pw2ywztsp5sf5ld85fm5w6s8d6eljdkzaddf3989lf2tdntj2hjc6wmskdye8qpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqdqm0m4" val eclair = mock[Eclair] - eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.send(any, 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", "maxFeeFlatSat" -> "10", "maxFeePct" -> "0.5").toEntity) ~> @@ -698,7 +698,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)(any[Timeout]).wasCalled(once) + eclair.send(None, 456 msat, any, any, Some(10 sat), Some(0.5), any, any)(any[Timeout]).wasCalled(once) } } @@ -706,7 +706,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val invoice = "lnbc12580n1pw2ywztsp59rnute7a5aa7hpcx0tvmje805wjveknzh2c2f36tfp3xlazrmzrspp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqkacgww" val eclair = mock[Eclair] - eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.send(any, 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", "maxFeeFlatSat" -> "10", "maxFeePct" -> "0.5", "pathFindingExperimentName" -> "my-test-experiment").toEntity) ~> @@ -715,7 +715,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), Some("my-test-experiment"))(any[Timeout]).wasCalled(once) + eclair.send(None, 456 msat, any, any, Some(10 sat), Some(0.5), Some("my-test-experiment"), any)(any[Timeout]).wasCalled(once) } } @@ -747,7 +747,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'sendtonode' 3") { val eclair = mock[Eclair] - eclair.sendWithPreimage(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.sendWithPreimage(any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) val mockService = new MockService(eclair) val remoteNodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") @@ -757,13 +757,13 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { assert(handled) assert(status == OK) - eclair.sendWithPreimage(None, remoteNodeId, 123 msat, any, None, None, None, None)(any[Timeout]).wasCalled(once) + eclair.sendWithPreimage(None, remoteNodeId, 123 msat, any, None, None, None, None, any)(any[Timeout]).wasCalled(once) } } test("'sendtonode' 4") { val eclair = mock[Eclair] - eclair.sendWithPreimage(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.sendWithPreimage(any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) val mockService = new MockService(eclair) val remoteNodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") @@ -773,13 +773,13 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { assert(handled) assert(status == OK) - eclair.sendWithPreimage(Some("42"), remoteNodeId, 123 msat, any, any, Some(10000 sat), Some(2.5), None)(any[Timeout]).wasCalled(once) + eclair.sendWithPreimage(Some("42"), remoteNodeId, 123 msat, any, any, Some(10000 sat), Some(2.5), None, any)(any[Timeout]).wasCalled(once) } } test("'sendtonode' 5") { val eclair = mock[Eclair] - eclair.sendWithPreimage(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.sendWithPreimage(any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) val mockService = new MockService(eclair) val remoteNodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") @@ -789,7 +789,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { assert(handled) assert(status == OK) - eclair.sendWithPreimage(None, remoteNodeId, 123 msat, any, None, None, None, Some("my-test-experiment"))(any[Timeout]).wasCalled(once) + eclair.sendWithPreimage(None, remoteNodeId, 123 msat, any, None, None, None, Some("my-test-experiment"), any)(any[Timeout]).wasCalled(once) } } @@ -1003,7 +1003,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val eclair = mock[Eclair] val mockService = new MockService(eclair) - eclair.findRoute(any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(Router.RouteResponse(Seq(Router.Route(456.msat, mockHops, None)))) + eclair.findRoute(any, any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(Router.RouteResponse(Seq(Router.Route(456.msat, mockHops, None)))) // invalid format Post("/findroute", FormData("format" -> "invalid-output-format", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> @@ -1013,7 +1013,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { assert(handled) assert(status == BadRequest) - eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any, any)(any[Timeout]).wasNever(called) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any, any, any)(any[Timeout]).wasNever(called) } // default format @@ -1026,7 +1026,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] matchTestJson("findroute-nodeid", response) - eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) } Post("/findroute", FormData("format" -> "nodeId", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> @@ -1038,7 +1038,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] matchTestJson("findroute-nodeid", response) - eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any, any)(any[Timeout]).wasCalled(twice) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any, any, any)(any[Timeout]).wasCalled(twice) } Post("/findroute", FormData("format" -> "shortChannelId", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> @@ -1050,7 +1050,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] matchTestJson("findroute-scid", response) - eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any, any)(any[Timeout]).wasCalled(threeTimes) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any, any, any)(any[Timeout]).wasCalled(threeTimes) } Post("/findroute", FormData("format" -> "full", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> @@ -1062,7 +1062,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] matchTestJson("findroute-full", response) - eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any, any)(any[Timeout]).wasCalled(fourTimes) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any, any, any)(any[Timeout]).wasCalled(fourTimes) } }