Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API: support query /channel info by short channel id #969

Merged
merged 5 commits into from
Apr 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
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
40 changes: 19 additions & 21 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import akka.pattern._
import akka.util.Timeout
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi}
import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.{NetworkFee, IncomingPayment, OutgoingPayment, Stats}
import fr.acinq.eclair.db.{IncomingPayment, NetworkFee, OutgoingPayment, Stats}
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.payment.PaymentLifecycle._
Expand All @@ -48,11 +49,11 @@ trait Eclair {

def forceClose(channelIdentifier: Either[ByteVector32, ShortChannelId])(implicit timeout: Timeout): Future[String]

def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String]
def updateRelayFee(channelIdentifier: Either[ByteVector32, ShortChannelId], feeBaseMsat: Long, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String]

def channelsInfo(toRemoteNode: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]]

def channelInfo(channelId: ByteVector32)(implicit timeout: Timeout): Future[RES_GETINFO]
def channelInfo(channelIdentifier: Either[ByteVector32, ShortChannelId])(implicit timeout: Timeout): Future[RES_GETINFO]

def peersInfo()(implicit timeout: Timeout): Future[Iterable[PeerInfo]]

Expand Down Expand Up @@ -109,15 +110,15 @@ class EclairImpl(appKit: Kit) extends Eclair {
}

override def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey: Option[ByteVector])(implicit timeout: Timeout): Future[String] = {
sendToChannel(channelIdentifier.fold[String](_.toString(), _.toString()), CMD_CLOSE(scriptPubKey)).mapTo[String]
sendToChannel(channelIdentifier, CMD_CLOSE(scriptPubKey)).mapTo[String]
}

override def forceClose(channelIdentifier: Either[ByteVector32, ShortChannelId])(implicit timeout: Timeout): Future[String] = {
sendToChannel(channelIdentifier.fold[String](_.toString(), _.toString()), CMD_FORCECLOSE).mapTo[String]
sendToChannel(channelIdentifier, CMD_FORCECLOSE).mapTo[String]
}

override def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] = {
sendToChannel(channelId, CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths)).mapTo[String]
override def updateRelayFee(channelIdentifier: Either[ByteVector32, ShortChannelId], feeBaseMsat: Long, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] = {
sendToChannel(channelIdentifier, CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths)).mapTo[String]
}

override def peersInfo()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] = for {
Expand All @@ -127,17 +128,17 @@ class EclairImpl(appKit: Kit) extends Eclair {

override def channelsInfo(toRemoteNode: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] = toRemoteNode match {
case Some(pk) => for {
channelsId <- (appKit.register ? 'channelsTo).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys)
channels <- Future.sequence(channelsId.map(channelId => sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO]))
channelIds <- (appKit.register ? 'channelsTo).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys)
channels <- Future.sequence(channelIds.map(channelId => sendToChannel(Left(channelId), CMD_GETINFO).mapTo[RES_GETINFO]))
} yield channels
case None => for {
channels_id <- (appKit.register ? 'channels).mapTo[Map[ByteVector32, ActorRef]].map(_.keys)
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toHex, CMD_GETINFO).mapTo[RES_GETINFO]))
channelIds <- (appKit.register ? 'channels).mapTo[Map[ByteVector32, ActorRef]].map(_.keys)
channels <- Future.sequence(channelIds.map(channelId => sendToChannel(Left(channelId), CMD_GETINFO).mapTo[RES_GETINFO]))
} yield channels
}

override def channelInfo(channelId: ByteVector32)(implicit timeout: Timeout): Future[RES_GETINFO] = {
sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO]
override def channelInfo(channelIdentifier: Either[ByteVector32, ShortChannelId])(implicit timeout: Timeout): Future[RES_GETINFO] = {
sendToChannel(channelIdentifier, CMD_GETINFO).mapTo[RES_GETINFO]
}

override def allNodes()(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]] = (appKit.router ? 'nodes).mapTo[Iterable[NodeAnnouncement]]
Expand Down Expand Up @@ -221,17 +222,14 @@ class EclairImpl(appKit: Kit) extends Eclair {
/**
* Sends a request to a channel and expects a response
*
* @param channelIdentifier can be a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded)
* @param channelIdentifier either a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded)
* @param request
* @return
*/
def sendToChannel(channelIdentifier: String, request: Any)(implicit timeout: Timeout): Future[Any] =
for {
fwdReq <- Future(Register.ForwardShortId(ShortChannelId(channelIdentifier), request))
.recoverWith { case _ => Future(Register.Forward(ByteVector32.fromValidHex(channelIdentifier), request)) }
.recoverWith { case _ => Future.failed(new RuntimeException(s"invalid channel identifier '$channelIdentifier'")) }
res <- appKit.register ? fwdReq
} yield res
def sendToChannel(channelIdentifier: Either[ByteVector32, ShortChannelId], request: Any)(implicit timeout: Timeout): Future[Any] = channelIdentifier match {
case Left(channelId) => appKit.register ? Forward(channelId, request)
case Right(shortChannelId) => appKit.register ? ForwardShortId(shortChannelId, request)
}

override def getInfoResponse()(implicit timeout: Timeout): Future[GetInfoResponse] = Future.successful(
GetInfoResponse(nodeId = appKit.nodeParams.nodeId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,28 @@ package fr.acinq.eclair.api
import akka.http.scaladsl.marshalling.ToResponseMarshaller
import akka.http.scaladsl.model.{ContentTypes, HttpResponse}
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.server.{Directives, Route}
import akka.http.scaladsl.server.{Directive1, Directives, MalformedFormFieldRejection, Route}
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.ShortChannelId
import fr.acinq.eclair.api.FormParamExtractors.{sha256HashUnmarshaller, shortChannelIdUnmarshaller}
import fr.acinq.eclair.api.JsonSupport._
import scala.concurrent.{Future}
import fr.acinq.eclair.payment.PaymentRequest
import scala.concurrent.Future
import scala.util.{Failure, Success}

trait ExtraDirectives extends Directives {

// named and typed URL parameters used across several routes
val shortChannelIdFormParam = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller)
val channelIdFormParam = "channelId".as[ByteVector32](sha256HashUnmarshaller)
val nodeIdFormParam = "nodeId".as[PublicKey]
val paymentHashFormParam = "paymentHash".as[ByteVector32](sha256HashUnmarshaller)
val fromFormParam = "from".as[Long]
val toFormParam = "to".as[Long]
val amountMsatFormParam = "amountMsat".as[Long]
val invoiceFormParam = "invoice".as[PaymentRequest]

// custom directive to fail with HTTP 404 (and JSON response) if the element was not found
def completeOrNotFound[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) {
case Success(Some(t)) => complete(t)
Expand All @@ -34,4 +49,11 @@ trait ExtraDirectives extends Directives {
case Failure(_) => reject
}

def withChannelIdentifier: Directive1[Either[ByteVector32, ShortChannelId]] = formFields(channelIdFormParam.?, shortChannelIdFormParam.?).tflatMap {
case (None, None) => reject(MalformedFormFieldRejection("channelId/shortChannelId", "Must specify either the channelId or shortChannelId"))
case (Some(channelId), None) => provide(Left(channelId))
case (None, Some(shortChannelId)) => provide(Right(shortChannelId))
case _ => reject(MalformedFormFieldRejection("channelId/shortChannelId", "Must specify either the channelId or shortChannelId"))
}

}
71 changes: 30 additions & 41 deletions eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import fr.acinq.eclair.{Eclair, ShortChannelId}
import grizzled.slf4j.Logging
import org.json4s.jackson.Serialization
import scodec.bits.ByteVector

import scala.concurrent.Future
import scala.concurrent.duration._

Expand Down Expand Up @@ -69,16 +68,6 @@ trait Service extends ExtraDirectives with Logging {
implicit val actorSystem: ActorSystem
implicit val mat: ActorMaterializer

// named and typed URL parameters used across several routes
val channelId = "channelId".as[ByteVector32](sha256HashUnmarshaller)
val nodeId = "nodeId".as[PublicKey]
val shortChannelId = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller)
val paymentHash = "paymentHash".as[ByteVector32](sha256HashUnmarshaller)
val from = "from".as[Long]
val to = "to".as[Long]
val amountMsat = "amountMsat".as[Long]
val invoice = "invoice".as[PaymentRequest]

val apiExceptionHandler = ExceptionHandler {
case t: Throwable =>
logger.error(s"API call failed with cause=${t.getMessage}", t)
Expand Down Expand Up @@ -148,46 +137,46 @@ trait Service extends ExtraDirectives with Logging {
path("connect") {
formFields("uri".as[String]) { uri =>
complete(eclairApi.connect(uri))
} ~ formFields(nodeId, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) =>
} ~ formFields(nodeIdFormParam, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) =>
complete(eclairApi.connect(s"$nodeId@$host:${port_opt.getOrElse(NodeURI.DEFAULT_PORT)}"))
}
} ~
path("open") {
formFields(nodeId, "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) {
formFields(nodeIdFormParam, "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) {
(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt) =>
complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt))
}
} ~
path("updaterelayfee") {
formFields(channelId, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) =>
complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional))
withChannelIdentifier { channelIdentifier =>
formFields("feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (feeBase, feeProportional) =>
complete(eclairApi.updateRelayFee(channelIdentifier, feeBase, feeProportional))
}
}
} ~
path("close") {
formFields(channelId, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) =>
complete(eclairApi.close(Left(channelId), scriptPubKey_opt))
} ~ formFields(shortChannelId, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) =>
complete(eclairApi.close(Right(shortChannelId), scriptPubKey_opt))
withChannelIdentifier { channelIdentifier =>
formFields("scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { scriptPubKey_opt =>
complete(eclairApi.close(channelIdentifier, scriptPubKey_opt))
}
}
} ~
path("forceclose") {
formFields(channelId) { channelId =>
complete(eclairApi.forceClose(Left(channelId)))
} ~ formFields(shortChannelId) { shortChannelId =>
complete(eclairApi.forceClose(Right(shortChannelId)))
withChannelIdentifier { channelIdentifier =>
complete(eclairApi.forceClose(channelIdentifier))
}
} ~
path("peers") {
complete(eclairApi.peersInfo())
} ~
path("channels") {
formFields(nodeId.?) { toRemoteNodeId_opt =>
formFields(nodeIdFormParam.?) { toRemoteNodeId_opt =>
complete(eclairApi.channelsInfo(toRemoteNodeId_opt))
}
} ~
path("channel") {
formFields(channelId) { channelId =>
complete(eclairApi.channelInfo(channelId))
withChannelIdentifier { channelIdentifier =>
complete(eclairApi.channelInfo(channelIdentifier))
}
} ~
path("allnodes") {
Expand All @@ -197,29 +186,29 @@ trait Service extends ExtraDirectives with Logging {
complete(eclairApi.allChannels())
} ~
path("allupdates") {
formFields(nodeId.?) { nodeId_opt =>
formFields(nodeIdFormParam.?) { nodeId_opt =>
complete(eclairApi.allUpdates(nodeId_opt))
}
} ~
path("findroute") {
formFields(invoice, amountMsat.?) {
formFields(invoiceFormParam, amountMsatFormParam.?) {
case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo))
case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo))
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'"))
}
} ~
path("findroutetonode") {
formFields(nodeId, amountMsat) { (nodeId, amount) =>
formFields(nodeIdFormParam, amountMsatFormParam) { (nodeId, amount) =>
complete(eclairApi.findRoute(nodeId, amount))
}
} ~
path("parseinvoice") {
formFields(invoice) { invoice =>
formFields(invoiceFormParam) { invoice =>
complete(invoice)
}
} ~
path("payinvoice") {
formFields(invoice, amountMsat.?, "maxAttempts".as[Int].?) {
formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?) {
case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts) =>
complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts))
case (invoice, Some(overrideAmount), maxAttempts) =>
Expand All @@ -228,51 +217,51 @@ trait Service extends ExtraDirectives with Logging {
}
} ~
path("sendtonode") {
formFields(amountMsat, paymentHash, nodeId, "maxAttempts".as[Int].?) { (amountMsat, paymentHash, nodeId, maxAttempts) =>
formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?) { (amountMsat, paymentHash, nodeId, maxAttempts) =>
complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts = maxAttempts))
}
} ~
path("getsentinfo") {
formFields("id".as[UUID]) { id =>
complete(eclairApi.sentInfo(Left(id)))
} ~ formFields(paymentHash) { paymentHash =>
} ~ formFields(paymentHashFormParam) { paymentHash =>
complete(eclairApi.sentInfo(Right(paymentHash)))
}
} ~
path("createinvoice") {
formFields("description".as[String], amountMsat.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?) { (desc, amountMsat, expire, fallBackAddress) =>
formFields("description".as[String], amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?) { (desc, amountMsat, expire, fallBackAddress) =>
complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress))
}
} ~
path("getinvoice") {
formFields(paymentHash) { paymentHash =>
formFields(paymentHashFormParam) { paymentHash =>
completeOrNotFound(eclairApi.getInvoice(paymentHash))
}
} ~
path("listinvoices") {
formFields(from.?, to.?) { (from_opt, to_opt) =>
formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) =>
complete(eclairApi.allInvoices(from_opt, to_opt))
}
} ~
path("listpendinginvoices") {
formFields(from.?, to.?) { (from_opt, to_opt) =>
formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) =>
complete(eclairApi.pendingInvoices(from_opt, to_opt))
}
} ~
path("getreceivedinfo") {
formFields(paymentHash) { paymentHash =>
formFields(paymentHashFormParam) { paymentHash =>
completeOrNotFound(eclairApi.receivedInfo(paymentHash))
} ~ formFields(invoice) { invoice =>
} ~ formFields(invoiceFormParam) { invoice =>
completeOrNotFound(eclairApi.receivedInfo(invoice.paymentHash))
}
} ~
path("audit") {
formFields(from.?, to.?) { (from_opt, to_opt) =>
formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) =>
complete(eclairApi.audit(from_opt, to_opt))
}
} ~
path("networkfees") {
formFields(from.?, to.?) { (from_opt, to_opt) =>
formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) =>
complete(eclairApi.networkFees(from_opt, to_opt))
}
} ~
Expand Down
Loading