Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. (#TODO)
Comment thread
thomash-acinq marked this conversation as resolved.
Outdated

### Miscellaneous improvements and bug fixes

Expand Down
22 changes: 18 additions & 4 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,15 +313,15 @@ 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))
assert(receive.expirySeconds_opt.contains(456))
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 =>
Expand All @@ -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))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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"))
}
}
Expand Down