1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-19 01:43:22 +01:00

Allow including routing hints when creating Bolt 11 invoice (#2909)

When nodes only have private channels, they must include routing hints
in their Bolt 11 invoices to be able to receive payments. We add a
parameter to the `createinvoice` RPC for this. Note that this may leak
the channel outpoint if `scid_alias` isn't used.

Fixes #2802
This commit is contained in:
Bastien Teinturier 2024-09-23 09:43:58 +02:00 committed by GitHub
parent 7b25c5adca
commit 885b45bd75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 26 additions and 10 deletions

View File

@ -27,6 +27,7 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup
### API changes
- `channelstats` now takes optional parameters `--count` and `--skip` to control pagination. By default, it will return first 10 entries. (#2890)
- `createinvoice` now takes an optional `--privateChannelIds` parameter that can be used to add routing hints through private channels. (#2909)
### Miscellaneous improvements and bug fixes

View File

@ -114,7 +114,7 @@ trait Eclair {
def nodes(nodeIds_opt: Option[Set[PublicKey]] = None)(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]]
def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[Bolt11Invoice]
def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32], privateChannelIds_opt: Option[List[ByteVector32]])(implicit timeout: Timeout): Future[Bolt11Invoice]
def newAddress(): Future[String]
@ -330,14 +330,28 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
}
}
override def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[Bolt11Invoice] = {
override def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32], privateChannelIds_opt: Option[List[ByteVector32]])(implicit timeout: Timeout): Future[Bolt11Invoice] = {
fallbackAddress_opt.foreach { fa =>
// If it's not a valid bitcoin address we throw an exception.
addressToPublicKeyScript(appKit.nodeParams.chainHash, fa) match {
case Left(failure) => throw new IllegalArgumentException(failure.toString)
case Right(_) => ()
}
} // if it's not a bitcoin address throws an exception
appKit.paymentHandler.toTyped.ask(ref => ReceiveStandardPayment(ref, amount_opt, description, expire_opt, fallbackAddress_opt = fallbackAddress_opt, paymentPreimage_opt = paymentPreimage_opt))
}
for {
routingHints <- getInvoiceRoutingHints(privateChannelIds_opt)
invoice <- appKit.paymentHandler.toTyped.ask[Bolt11Invoice](ref => ReceiveStandardPayment(ref, amount_opt, description, expire_opt, routingHints, fallbackAddress_opt, paymentPreimage_opt))
} yield invoice
}
private def getInvoiceRoutingHints(privateChannelIds_opt: Option[List[ByteVector32]])(implicit timeout: Timeout): Future[List[List[Bolt11Invoice.ExtraHop]]] = {
privateChannelIds_opt match {
case Some(channelIds) =>
(appKit.router ? GetRouterData).mapTo[Router.Data].map {
d => channelIds.flatMap(cid => d.privateChannels.get(cid)).flatMap(_.toIncomingExtraHop).map(hop => hop :: Nil)
}
case None => Future.successful(Nil)
}
}
override def newAddress(): Future[String] = {

View File

@ -313,7 +313,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
val fallBackAddressRaw = "muhtvdmsnbQEPFuEmxcChX58fGvXaaUoVt"
val eclair = new EclairImpl(kit)
eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some(fallBackAddressRaw), None)
eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some(fallBackAddressRaw), None, None)
val receive = paymentHandler.expectMsgType[ReceiveStandardPayment]
assert(receive.amount_opt.contains(123 msat))
@ -321,7 +321,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
assert(receive.fallbackAddress_opt.contains(fallBackAddressRaw))
// try with wrong address format
assertThrows[IllegalArgumentException](eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some("wassa wassa"), None))
assertThrows[IllegalArgumentException](eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some("wassa wassa"), None, None))
}
test("passing a payment_preimage to /createinvoice should result in an invoice with payment_hash=H(payment_preimage)") { f =>
@ -331,7 +331,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
val eclair = new EclairImpl(kitWithPaymentHandler)
val paymentPreimage = randomBytes32()
eclair.receive(Left("some desc"), None, None, None, Some(paymentPreimage)).pipeTo(sender.ref)
eclair.receive(Left("some desc"), None, None, None, Some(paymentPreimage), None).pipeTo(sender.ref)
assert(sender.expectMsgType[Invoice].paymentHash == Crypto.sha256(paymentPreimage))
}

View File

@ -18,6 +18,7 @@ package fr.acinq.eclair.api.handlers
import akka.http.scaladsl.server.Route
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.api.Service
import fr.acinq.eclair.api.directives.EclairDirectives
import fr.acinq.eclair.api.serde.FormParamExtractors._
@ -28,9 +29,9 @@ trait Invoice {
import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization}
val createInvoice: Route = postRequest("createinvoice") { implicit t =>
formFields("description".as[String].?, "descriptionHash".as[ByteVector32].?, amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[ByteVector32](bytes32Unmarshaller).?) {
case (Some(desc), None, amountMsat, expire, fallBackAddress, paymentPreimage_opt) => complete(eclairApi.receive(Left(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt))
case (None, Some(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt) => complete(eclairApi.receive(Right(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt))
formFields("description".as[String].?, "descriptionHash".as[ByteVector32].?, amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[ByteVector32](bytes32Unmarshaller).?, "privateChannelIds".as[List[ByteVector32]](bytes32ListUnmarshaller).?) {
case (Some(desc), None, amountMsat, expire, fallBackAddress, paymentPreimage_opt, privateChannelIds_opt) => complete(eclairApi.receive(Left(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt, privateChannelIds_opt))
case (None, Some(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt, privateChannelIds_opt) => complete(eclairApi.receive(Right(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt, privateChannelIds_opt))
case _ => failWith(new RuntimeException("Either 'description' (string) or 'descriptionHash' (sha256 hash of description string) must be supplied"))
}
}