From 37a783c119e24f8857a6fd8a744a28d592cfe47b Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 23 Nov 2020 12:14:04 +0200 Subject: [PATCH 1/6] Move reusable API parts to a trait --- .../scala/fr/acinq/eclair/api/Service.scala | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala index ac0fc54701..fcb915d351 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -48,16 +48,14 @@ import scala.concurrent.duration._ case class ErrorResponse(error: String) -trait Service extends ExtraDirectives with Logging { +// important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 - // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 +import JsonSupport.{formats, marshaller, serialization} - import JsonSupport.{formats, marshaller, serialization} +trait AbstractService extends ExtraDirectives with Logging { def password: String - val eclairApi: Eclair - implicit val actorSystem: ActorSystem implicit val mat: Materializer @@ -76,13 +74,27 @@ trait Service extends ExtraDirectives with Logging { // map all the rejections to a JSON error object ErrorResponse val apiRejectionHandler = RejectionHandler.default.mapRejectionResponse { case res@HttpResponse(_, _, ent: HttpEntity.Strict, _) => - res.copy(entity = HttpEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse(ent.data.utf8String)))) + res.withEntity(HttpEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse(ent.data.utf8String)))) } val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") :: `Access-Control-Allow-Methods`(POST) :: `Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil + val timeoutResponse: HttpRequest => HttpResponse = { _ => + HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("request timed out"))) + } + + def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { + case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) + case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None))(actorSystem.dispatcher) // force a 1 sec pause to deter brute force + } +} + +trait Service extends AbstractService { + + val eclairApi: Eclair + lazy val makeSocketHandler: Flow[Message, TextMessage.Strict, NotUsed] = { // create a flow transforming a queue of string -> string @@ -116,15 +128,6 @@ trait Service extends ExtraDirectives with Logging { .map(TextMessage.apply) } - val timeoutResponse: HttpRequest => HttpResponse = { _ => - HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("request timed out"))) - } - - def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { - case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) - case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None))(actorSystem.dispatcher) // force a 1 sec pause to deter brute force - } - val route: Route = { respondWithDefaultHeaders(customHeaders) { handleExceptions(apiExceptionHandler) { From 020684bf5931ba8164db01eb0480541cc75d1ef5 Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 23 Nov 2020 12:23:19 +0200 Subject: [PATCH 2/6] Fix indent --- .../scala/fr/acinq/eclair/api/Service.scala | 382 +++++++++--------- 1 file changed, 191 insertions(+), 191 deletions(-) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala index fcb915d351..e76f8de00b 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -145,205 +145,205 @@ trait Service extends AbstractService { path("getinfo") { complete(eclairApi.getInfo()) } ~ - path("connect") { - formFields("uri".as[NodeURI]) { uri => - complete(eclairApi.connect(Left(uri))) - } ~ formFields(nodeIdFormParam, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) => - complete(eclairApi.connect(Left(NodeURI(nodeId, HostAndPort.fromParts(host, port_opt.getOrElse(NodeURI.DEFAULT_PORT)))))) - } ~ formFields(nodeIdFormParam) { nodeId => - complete(eclairApi.connect(Right(nodeId))) - } - } ~ - path("disconnect") { - formFields(nodeIdFormParam) { nodeId => - complete(eclairApi.disconnect(nodeId)) + path("connect") { + formFields("uri".as[NodeURI]) { uri => + complete(eclairApi.connect(Left(uri))) + } ~ formFields(nodeIdFormParam, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) => + complete(eclairApi.connect(Left(NodeURI(nodeId, HostAndPort.fromParts(host, port_opt.getOrElse(NodeURI.DEFAULT_PORT)))))) + } ~ formFields(nodeIdFormParam) { nodeId => + complete(eclairApi.connect(Right(nodeId))) + } + } ~ + path("disconnect") { + formFields(nodeIdFormParam) { nodeId => + complete(eclairApi.disconnect(nodeId)) + } + } ~ + path("open") { + formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "fundingFeerateSatByte".as[FeeratePerByte].?, "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") { + withChannelsIdentifier { channels => + formFields("feeBaseMsat".as[MilliSatoshi], "feeProportionalMillionths".as[Long]) { (feeBase, feeProportional) => + complete(eclairApi.updateRelayFee(channels, feeBase, feeProportional)) } - } ~ - path("open") { - formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "fundingFeerateSatByte".as[FeeratePerByte].?, "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) { - (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt) => - complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt)) + } + } ~ + path("close") { + withChannelsIdentifier { channels => + formFields("scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { scriptPubKey_opt => + complete(eclairApi.close(channels, scriptPubKey_opt)) } - } ~ - path("updaterelayfee") { - withChannelsIdentifier { channels => - formFields("feeBaseMsat".as[MilliSatoshi], "feeProportionalMillionths".as[Long]) { (feeBase, feeProportional) => - complete(eclairApi.updateRelayFee(channels, feeBase, feeProportional)) + } + } ~ + path("forceclose") { + withChannelsIdentifier { channels => + complete(eclairApi.forceClose(channels)) + } + } ~ + path("peers") { + complete(eclairApi.peers()) + } ~ + path("nodes") { + formFields(nodeIdsFormParam.?) { nodeIds_opt => + complete(eclairApi.nodes(nodeIds_opt.map(_.toSet))) + } + } ~ + path("channels") { + formFields(nodeIdFormParam.?) { toRemoteNodeId_opt => + complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + withChannelIdentifier { channel => + complete(eclairApi.channelInfo(channel)) + } + } ~ + path("allchannels") { + complete(eclairApi.allChannels()) + } ~ + path("networkstats") { + complete(eclairApi.networkStats()) + } ~ + path("allupdates") { + formFields(nodeIdFormParam.?) { nodeId_opt => + complete(eclairApi.allUpdates(nodeId_opt)) + } + } ~ + path("findroute") { + formFields(invoiceFormParam, amountMsatFormParam.?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount, 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(nodeIdFormParam, amountMsatFormParam) { (nodeId, amount) => + complete(eclairApi.findRoute(nodeId, amount)) + } + } ~ + path("parseinvoice") { + formFields(invoiceFormParam) { invoice => + complete(invoice) + } + } ~ + path("payinvoice") { + formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => + complete(eclairApi.send(externalId_opt, nodeId, amount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) + case (invoice, Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => + complete(eclairApi.send(externalId_opt, invoice.nodeId, overrideAmount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) + } + } ~ + path("sendtonode") { + formFields(amountMsatFormParam, nodeIdFormParam, paymentHashFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "keysend".as[Boolean].?) { + case (amountMsat, nodeId, Some(paymentHash), maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, keySend) => + keySend match { + case Some(true) => reject(MalformedFormFieldRejection("paymentHash", "You cannot request a KeySend payment and specify a paymentHash")) + case _ => complete(eclairApi.send(externalId_opt, nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) } - } - } ~ - path("close") { - withChannelsIdentifier { channels => - formFields("scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { scriptPubKey_opt => - complete(eclairApi.close(channels, scriptPubKey_opt)) + case (amountMsat, nodeId, None, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, keySend) => + keySend match { + case Some(true) => complete(eclairApi.sendWithPreimage(externalId_opt, nodeId, amountMsat, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) + case _ => reject(MalformedFormFieldRejection("paymentHash", "No payment type specified. Either provide a paymentHash or use --keysend=true")) } - } - } ~ - path("forceclose") { - withChannelsIdentifier { channels => - complete(eclairApi.forceClose(channels)) - } - } ~ - path("peers") { - complete(eclairApi.peers()) - } ~ - path("nodes") { - formFields(nodeIdsFormParam.?) { nodeIds_opt => - complete(eclairApi.nodes(nodeIds_opt.map(_.toSet))) - } - } ~ - path("channels") { - formFields(nodeIdFormParam.?) { toRemoteNodeId_opt => - complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) - } - } ~ - path("channel") { - withChannelIdentifier { channel => - complete(eclairApi.channelInfo(channel)) - } - } ~ - path("allchannels") { - complete(eclairApi.allChannels()) - } ~ - path("networkstats") { - complete(eclairApi.networkStats()) - } ~ - path("allupdates") { - formFields(nodeIdFormParam.?) { nodeId_opt => - complete(eclairApi.allUpdates(nodeId_opt)) - } - } ~ - path("findroute") { - formFields(invoiceFormParam, amountMsatFormParam.?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount, 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(nodeIdFormParam, amountMsatFormParam) { (nodeId, amount) => - complete(eclairApi.findRoute(nodeId, amount)) - } - } ~ - path("parseinvoice") { - formFields(invoiceFormParam) { invoice => - complete(invoice) - } - } ~ - path("payinvoice") { - formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => - complete(eclairApi.send(externalId_opt, nodeId, amount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) - case (invoice, Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => - complete(eclairApi.send(externalId_opt, invoice.nodeId, overrideAmount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) - case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) - } - } ~ - path("sendtonode") { - formFields(amountMsatFormParam, nodeIdFormParam, paymentHashFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "keysend".as[Boolean].?) { - case (amountMsat, nodeId, Some(paymentHash), maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, keySend) => - keySend match { - case Some(true) => reject(MalformedFormFieldRejection("paymentHash", "You cannot request a KeySend payment and specify a paymentHash")) - case _ => complete(eclairApi.send(externalId_opt, nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) - } - case (amountMsat, nodeId, None, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, keySend) => - keySend match { - case Some(true) => complete(eclairApi.sendWithPreimage(externalId_opt, nodeId, amountMsat, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) - case _ => reject(MalformedFormFieldRejection("paymentHash", "No payment type specified. Either provide a paymentHash or use --keysend=true")) - } - } - } ~ - path("sendtoroute") { - withRoute { hops => - formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "finalCltvExpiry".as[Int], "externalId".?, "parentId".as[UUID].?, "trampolineSecret".as[ByteVector32].?, "trampolineFeesMsat".as[MilliSatoshi].?, "trampolineCltvExpiry".as[Int].?, "trampolineNodes".as[List[PublicKey]](pubkeyListUnmarshaller).?) { - (amountMsat, recipientAmountMsat_opt, invoice, finalCltvExpiry, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) => { - val route = hops match { - case Left(shortChannelIds) => PredefinedChannelRoute(invoice.nodeId, shortChannelIds) - case Right(nodeIds) => PredefinedNodeRoute(nodeIds) - } - complete(eclairApi.sendToRoute(amountMsat, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, CltvExpiryDelta(finalCltvExpiry), route, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt.map(CltvExpiryDelta), trampolineNodes_opt.getOrElse(Nil))) + } + } ~ + path("sendtoroute") { + withRoute { hops => + formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "finalCltvExpiry".as[Int], "externalId".?, "parentId".as[UUID].?, "trampolineSecret".as[ByteVector32].?, "trampolineFeesMsat".as[MilliSatoshi].?, "trampolineCltvExpiry".as[Int].?, "trampolineNodes".as[List[PublicKey]](pubkeyListUnmarshaller).?) { + (amountMsat, recipientAmountMsat_opt, invoice, finalCltvExpiry, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) => { + val route = hops match { + case Left(shortChannelIds) => PredefinedChannelRoute(invoice.nodeId, shortChannelIds) + case Right(nodeIds) => PredefinedNodeRoute(nodeIds) } + complete(eclairApi.sendToRoute(amountMsat, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, CltvExpiryDelta(finalCltvExpiry), route, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt.map(CltvExpiryDelta), trampolineNodes_opt.getOrElse(Nil))) } } - } ~ - path("sendonchain") { - formFields("address".as[String], "amountSatoshis".as[Satoshi], "confirmationTarget".as[Long]) { (address, amount, confirmationTarget) => - complete(eclairApi.sendOnChain(address, amount, confirmationTarget)) - } - } ~ - path("getsentinfo") { - formFields("id".as[UUID]) { id => - complete(eclairApi.sentInfo(Left(id))) - } ~ formFields(paymentHashFormParam) { paymentHash => - complete(eclairApi.sentInfo(Right(paymentHash))) - } - } ~ - path("createinvoice") { - formFields("description".as[String], amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[ByteVector32](sha256HashUnmarshaller).?) { (desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt) => - complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt)) - } - } ~ - path("getinvoice") { - formFields(paymentHashFormParam) { paymentHash => - completeOrNotFound(eclairApi.getInvoice(paymentHash)) - } - } ~ - path("listinvoices") { - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.allInvoices(from_opt, to_opt)) - } - } ~ - path("listpendinginvoices") { - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.pendingInvoices(from_opt, to_opt)) - } - } ~ - path("getreceivedinfo") { - formFields(paymentHashFormParam) { paymentHash => - completeOrNotFound(eclairApi.receivedInfo(paymentHash)) - } ~ formFields(invoiceFormParam) { invoice => - completeOrNotFound(eclairApi.receivedInfo(invoice.paymentHash)) - } - } ~ - path("audit") { - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.audit(from_opt, to_opt)) - } - } ~ - path("networkfees") { - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.networkFees(from_opt, to_opt)) - } - } ~ - path("channelstats") { - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.channelStats(from_opt, to_opt)) - } - } ~ - path("usablebalances") { - complete(eclairApi.usableBalances()) - } ~ - path("onchainbalance") { - complete(eclairApi.onChainBalance()) - } ~ - path("getnewaddress") { - complete(eclairApi.newAddress()) - } ~ - path("onchaintransactions") { - formFields("count".as[Int].?, "skip".as[Int].?) { (count_opt, skip_opt) => - complete(eclairApi.onChainTransactions(count_opt.getOrElse(10), skip_opt.getOrElse(0))) - } - } ~ - path("signmessage") { - formFields("msg".as[ByteVector](base64DataUnmarshaller)) { message => - complete(eclairApi.signMessage(message)) - } - } ~ - path("verifymessage") { - formFields("msg".as[ByteVector](base64DataUnmarshaller), "sig".as[ByteVector](binaryDataUnmarshaller)) { (message, signature) => - complete(eclairApi.verifyMessage(message, signature)) - } } + } ~ + path("sendonchain") { + formFields("address".as[String], "amountSatoshis".as[Satoshi], "confirmationTarget".as[Long]) { (address, amount, confirmationTarget) => + complete(eclairApi.sendOnChain(address, amount, confirmationTarget)) + } + } ~ + path("getsentinfo") { + formFields("id".as[UUID]) { id => + complete(eclairApi.sentInfo(Left(id))) + } ~ formFields(paymentHashFormParam) { paymentHash => + complete(eclairApi.sentInfo(Right(paymentHash))) + } + } ~ + path("createinvoice") { + formFields("description".as[String], amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[ByteVector32](sha256HashUnmarshaller).?) { (desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt) => + complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt)) + } + } ~ + path("getinvoice") { + formFields(paymentHashFormParam) { paymentHash => + completeOrNotFound(eclairApi.getInvoice(paymentHash)) + } + } ~ + path("listinvoices") { + formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => + complete(eclairApi.allInvoices(from_opt, to_opt)) + } + } ~ + path("listpendinginvoices") { + formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => + complete(eclairApi.pendingInvoices(from_opt, to_opt)) + } + } ~ + path("getreceivedinfo") { + formFields(paymentHashFormParam) { paymentHash => + completeOrNotFound(eclairApi.receivedInfo(paymentHash)) + } ~ formFields(invoiceFormParam) { invoice => + completeOrNotFound(eclairApi.receivedInfo(invoice.paymentHash)) + } + } ~ + path("audit") { + formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => + complete(eclairApi.audit(from_opt, to_opt)) + } + } ~ + path("networkfees") { + formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => + complete(eclairApi.networkFees(from_opt, to_opt)) + } + } ~ + path("channelstats") { + formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => + complete(eclairApi.channelStats(from_opt, to_opt)) + } + } ~ + path("usablebalances") { + complete(eclairApi.usableBalances()) + } ~ + path("onchainbalance") { + complete(eclairApi.onChainBalance()) + } ~ + path("getnewaddress") { + complete(eclairApi.newAddress()) + } ~ + path("onchaintransactions") { + formFields("count".as[Int].?, "skip".as[Int].?) { (count_opt, skip_opt) => + complete(eclairApi.onChainTransactions(count_opt.getOrElse(10), skip_opt.getOrElse(0))) + } + } ~ + path("signmessage") { + formFields("msg".as[ByteVector](base64DataUnmarshaller)) { message => + complete(eclairApi.signMessage(message)) + } + } ~ + path("verifymessage") { + formFields("msg".as[ByteVector](base64DataUnmarshaller), "sig".as[ByteVector](binaryDataUnmarshaller)) { (message, signature) => + complete(eclairApi.verifyMessage(message, signature)) + } + } } ~ get { path("ws") { handleWebSocketMessages(makeSocketHandler) From b624e5093be99f7bfd6a17d9a870dac04a2fc8f9 Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 23 Nov 2020 13:46:11 +0200 Subject: [PATCH 3/6] Remove materializer --- eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala | 4 +--- eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala | 3 +-- .../src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 2 -- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala index 3c834d2492..e7d28ad052 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala @@ -20,7 +20,7 @@ import java.io.File import akka.actor.ActorSystem import akka.http.scaladsl.Http -import akka.stream.{ActorMaterializer, BindFailedException} +import akka.stream.BindFailedException import fr.acinq.eclair.api.Service import grizzled.slf4j.Logging import kamon.Kamon @@ -69,14 +69,12 @@ object Boot extends App with Logging { val config = system.settings.config.getConfig("eclair") if(config.getBoolean("api.enabled")){ logger.info(s"json API enabled on port=${config.getInt("api.port")}") - implicit val materializer = ActorMaterializer() val apiPassword = config.getString("api.password") match { case "" => throw EmptyAPIPasswordException case valid => valid } val apiRoute = new Service { override val actorSystem = system - override val mat = materializer override val password = apiPassword override val eclairApi: Eclair = new EclairImpl(kit) }.route diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala index e76f8de00b..8e30008d6d 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -28,7 +28,7 @@ import akka.http.scaladsl.model.ws.{Message, TextMessage} import akka.http.scaladsl.server._ import akka.http.scaladsl.server.directives.Credentials import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} -import akka.stream.{Materializer, OverflowStrategy} +import akka.stream.OverflowStrategy import akka.util.Timeout import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey @@ -57,7 +57,6 @@ trait AbstractService extends ExtraDirectives with Logging { def password: String implicit val actorSystem: ActorSystem - implicit val mat: Materializer // timeout for reading request parameters from the underlying stream val paramParsingTimeout = 5 seconds 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 dfb8c93d6a..16e8923ea3 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 @@ -24,7 +24,6 @@ import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.model.headers.BasicHttpCredentials import akka.http.scaladsl.server.Route import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest, WSProbe} -import akka.stream.Materializer import akka.util.Timeout import de.heikoseeberger.akkahttpjson4s.Json4sSupport import fr.acinq.bitcoin.Crypto.PublicKey @@ -71,7 +70,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM override def password: String = "mock" override implicit val actorSystem: ActorSystem = system - override implicit val mat: Materializer = materializer } test("API service should handle failures correctly") { From 427fa66f617005d895cc4d45ecdd76824ff91b56 Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 23 Nov 2020 13:55:35 +0200 Subject: [PATCH 4/6] Update http init methods --- eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala index e7d28ad052..a0df793249 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala @@ -78,7 +78,7 @@ object Boot extends App with Logging { override val password = apiPassword override val eclairApi: Eclair = new EclairImpl(kit) }.route - Http().bindAndHandle(apiRoute, config.getString("api.binding-ip"), config.getInt("api.port")).recover { + Http().newServerAt(config.getString("api.binding-ip"), config.getInt("api.port")).bindFlow(apiRoute).recover { case _: BindFailedException => onError(TCPBindException(config.getInt("api.port"))) } } else { From 8261291aed7fb3f9e76f97fa638b26a4d49b934a Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 23 Nov 2020 14:47:16 +0200 Subject: [PATCH 5/6] Split boilerplate code Plugins won't need to re-implement splitted part and API rules will be more or less standard across base and plugins. --- .../src/main/scala/fr/acinq/eclair/Boot.scala | 2 +- .../scala/fr/acinq/eclair/api/Service.scala | 451 +++++++++--------- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 54 +-- 3 files changed, 256 insertions(+), 251 deletions(-) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala index a0df793249..4dd0b38580 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala @@ -77,7 +77,7 @@ object Boot extends App with Logging { override val actorSystem = system override val password = apiPassword override val eclairApi: Eclair = new EclairImpl(kit) - }.route + }.finalRoute Http().newServerAt(config.getString("api.binding-ip"), config.getInt("api.port")).bindFlow(apiRoute).recover { case _: BindFailedException => onError(TCPBindException(config.getInt("api.port"))) } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala index 8e30008d6d..04f4c6fe51 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -84,10 +84,35 @@ trait AbstractService extends ExtraDirectives with Logging { HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("request timed out"))) } + val finalRoute: Route = + respondWithDefaultHeaders(customHeaders) { + handleExceptions(apiExceptionHandler) { + handleRejections(apiRejectionHandler) { + // forcing the request entity to be fully parsed can have performance issues, see: https://doc.akka.io/docs/akka-http/current/routing-dsl/directives/basic-directives/toStrictEntity.html#description + toStrictEntity(paramParsingTimeout) { + formFields("timeoutSeconds".as[Timeout].?) { tm_opt => + // this is the akka timeout + val timeout: Timeout = tm_opt.getOrElse(Timeout(30 seconds)) + // we ensure that http timeout is greater than akka timeout + withRequestTimeout(timeout.duration + 2.seconds) { + withRequestTimeoutResponse(timeoutResponse) { + authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => + route(timeout) + } + } + } + } + } + } + } + } + def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None))(actorSystem.dispatcher) // force a 1 sec pause to deter brute force } + + def route(implicit timeout: Timeout): Route } trait Service extends AbstractService { @@ -127,233 +152,213 @@ trait Service extends AbstractService { .map(TextMessage.apply) } - val route: Route = { - respondWithDefaultHeaders(customHeaders) { - handleExceptions(apiExceptionHandler) { - handleRejections(apiRejectionHandler) { - // forcing the request entity to be fully parsed can have performance issues, see: https://doc.akka.io/docs/akka-http/current/routing-dsl/directives/basic-directives/toStrictEntity.html#description - toStrictEntity(paramParsingTimeout) { - formFields("timeoutSeconds".as[Timeout].?) { tm_opt => - // this is the akka timeout - implicit val timeout: Timeout = tm_opt.getOrElse(Timeout(30 seconds)) - // we ensure that http timeout is greater than akka timeout - withRequestTimeout(timeout.duration + 2.seconds) { - withRequestTimeoutResponse(timeoutResponse) { - authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => - post { - path("getinfo") { - complete(eclairApi.getInfo()) - } ~ - path("connect") { - formFields("uri".as[NodeURI]) { uri => - complete(eclairApi.connect(Left(uri))) - } ~ formFields(nodeIdFormParam, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) => - complete(eclairApi.connect(Left(NodeURI(nodeId, HostAndPort.fromParts(host, port_opt.getOrElse(NodeURI.DEFAULT_PORT)))))) - } ~ formFields(nodeIdFormParam) { nodeId => - complete(eclairApi.connect(Right(nodeId))) - } - } ~ - path("disconnect") { - formFields(nodeIdFormParam) { nodeId => - complete(eclairApi.disconnect(nodeId)) - } - } ~ - path("open") { - formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "fundingFeerateSatByte".as[FeeratePerByte].?, "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") { - withChannelsIdentifier { channels => - formFields("feeBaseMsat".as[MilliSatoshi], "feeProportionalMillionths".as[Long]) { (feeBase, feeProportional) => - complete(eclairApi.updateRelayFee(channels, feeBase, feeProportional)) - } - } - } ~ - path("close") { - withChannelsIdentifier { channels => - formFields("scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { scriptPubKey_opt => - complete(eclairApi.close(channels, scriptPubKey_opt)) - } - } - } ~ - path("forceclose") { - withChannelsIdentifier { channels => - complete(eclairApi.forceClose(channels)) - } - } ~ - path("peers") { - complete(eclairApi.peers()) - } ~ - path("nodes") { - formFields(nodeIdsFormParam.?) { nodeIds_opt => - complete(eclairApi.nodes(nodeIds_opt.map(_.toSet))) - } - } ~ - path("channels") { - formFields(nodeIdFormParam.?) { toRemoteNodeId_opt => - complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) - } - } ~ - path("channel") { - withChannelIdentifier { channel => - complete(eclairApi.channelInfo(channel)) - } - } ~ - path("allchannels") { - complete(eclairApi.allChannels()) - } ~ - path("networkstats") { - complete(eclairApi.networkStats()) - } ~ - path("allupdates") { - formFields(nodeIdFormParam.?) { nodeId_opt => - complete(eclairApi.allUpdates(nodeId_opt)) - } - } ~ - path("findroute") { - formFields(invoiceFormParam, amountMsatFormParam.?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount, 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(nodeIdFormParam, amountMsatFormParam) { (nodeId, amount) => - complete(eclairApi.findRoute(nodeId, amount)) - } - } ~ - path("parseinvoice") { - formFields(invoiceFormParam) { invoice => - complete(invoice) - } - } ~ - path("payinvoice") { - formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => - complete(eclairApi.send(externalId_opt, nodeId, amount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) - case (invoice, Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => - complete(eclairApi.send(externalId_opt, invoice.nodeId, overrideAmount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) - case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) - } - } ~ - path("sendtonode") { - formFields(amountMsatFormParam, nodeIdFormParam, paymentHashFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "keysend".as[Boolean].?) { - case (amountMsat, nodeId, Some(paymentHash), maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, keySend) => - keySend match { - case Some(true) => reject(MalformedFormFieldRejection("paymentHash", "You cannot request a KeySend payment and specify a paymentHash")) - case _ => complete(eclairApi.send(externalId_opt, nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) - } - case (amountMsat, nodeId, None, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, keySend) => - keySend match { - case Some(true) => complete(eclairApi.sendWithPreimage(externalId_opt, nodeId, amountMsat, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) - case _ => reject(MalformedFormFieldRejection("paymentHash", "No payment type specified. Either provide a paymentHash or use --keysend=true")) - } - } - } ~ - path("sendtoroute") { - withRoute { hops => - formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "finalCltvExpiry".as[Int], "externalId".?, "parentId".as[UUID].?, "trampolineSecret".as[ByteVector32].?, "trampolineFeesMsat".as[MilliSatoshi].?, "trampolineCltvExpiry".as[Int].?, "trampolineNodes".as[List[PublicKey]](pubkeyListUnmarshaller).?) { - (amountMsat, recipientAmountMsat_opt, invoice, finalCltvExpiry, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) => { - val route = hops match { - case Left(shortChannelIds) => PredefinedChannelRoute(invoice.nodeId, shortChannelIds) - case Right(nodeIds) => PredefinedNodeRoute(nodeIds) - } - complete(eclairApi.sendToRoute(amountMsat, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, CltvExpiryDelta(finalCltvExpiry), route, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt.map(CltvExpiryDelta), trampolineNodes_opt.getOrElse(Nil))) - } - } - } - } ~ - path("sendonchain") { - formFields("address".as[String], "amountSatoshis".as[Satoshi], "confirmationTarget".as[Long]) { (address, amount, confirmationTarget) => - complete(eclairApi.sendOnChain(address, amount, confirmationTarget)) - } - } ~ - path("getsentinfo") { - formFields("id".as[UUID]) { id => - complete(eclairApi.sentInfo(Left(id))) - } ~ formFields(paymentHashFormParam) { paymentHash => - complete(eclairApi.sentInfo(Right(paymentHash))) - } - } ~ - path("createinvoice") { - formFields("description".as[String], amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[ByteVector32](sha256HashUnmarshaller).?) { (desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt) => - complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt)) - } - } ~ - path("getinvoice") { - formFields(paymentHashFormParam) { paymentHash => - completeOrNotFound(eclairApi.getInvoice(paymentHash)) - } - } ~ - path("listinvoices") { - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.allInvoices(from_opt, to_opt)) - } - } ~ - path("listpendinginvoices") { - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.pendingInvoices(from_opt, to_opt)) - } - } ~ - path("getreceivedinfo") { - formFields(paymentHashFormParam) { paymentHash => - completeOrNotFound(eclairApi.receivedInfo(paymentHash)) - } ~ formFields(invoiceFormParam) { invoice => - completeOrNotFound(eclairApi.receivedInfo(invoice.paymentHash)) - } - } ~ - path("audit") { - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.audit(from_opt, to_opt)) - } - } ~ - path("networkfees") { - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.networkFees(from_opt, to_opt)) - } - } ~ - path("channelstats") { - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.channelStats(from_opt, to_opt)) - } - } ~ - path("usablebalances") { - complete(eclairApi.usableBalances()) - } ~ - path("onchainbalance") { - complete(eclairApi.onChainBalance()) - } ~ - path("getnewaddress") { - complete(eclairApi.newAddress()) - } ~ - path("onchaintransactions") { - formFields("count".as[Int].?, "skip".as[Int].?) { (count_opt, skip_opt) => - complete(eclairApi.onChainTransactions(count_opt.getOrElse(10), skip_opt.getOrElse(0))) - } - } ~ - path("signmessage") { - formFields("msg".as[ByteVector](base64DataUnmarshaller)) { message => - complete(eclairApi.signMessage(message)) - } - } ~ - path("verifymessage") { - formFields("msg".as[ByteVector](base64DataUnmarshaller), "sig".as[ByteVector](binaryDataUnmarshaller)) { (message, signature) => - complete(eclairApi.verifyMessage(message, signature)) - } - } - } ~ get { - path("ws") { - handleWebSocketMessages(makeSocketHandler) - } - } - } - } + override def route(implicit timeout: Timeout): Route = { + post { + path("getinfo") { + complete(eclairApi.getInfo()) + } ~ + path("connect") { + formFields("uri".as[NodeURI]) { uri => + complete(eclairApi.connect(Left(uri))) + } ~ formFields(nodeIdFormParam, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) => + complete(eclairApi.connect(Left(NodeURI(nodeId, HostAndPort.fromParts(host, port_opt.getOrElse(NodeURI.DEFAULT_PORT)))))) + } ~ formFields(nodeIdFormParam) { nodeId => + complete(eclairApi.connect(Right(nodeId))) + } + } ~ + path("disconnect") { + formFields(nodeIdFormParam) { nodeId => + complete(eclairApi.disconnect(nodeId)) + } + } ~ + path("open") { + formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "fundingFeerateSatByte".as[FeeratePerByte].?, "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") { + withChannelsIdentifier { channels => + formFields("feeBaseMsat".as[MilliSatoshi], "feeProportionalMillionths".as[Long]) { (feeBase, feeProportional) => + complete(eclairApi.updateRelayFee(channels, feeBase, feeProportional)) + } + } + } ~ + path("close") { + withChannelsIdentifier { channels => + formFields("scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { scriptPubKey_opt => + complete(eclairApi.close(channels, scriptPubKey_opt)) + } + } + } ~ + path("forceclose") { + withChannelsIdentifier { channels => + complete(eclairApi.forceClose(channels)) + } + } ~ + path("peers") { + complete(eclairApi.peers()) + } ~ + path("nodes") { + formFields(nodeIdsFormParam.?) { nodeIds_opt => + complete(eclairApi.nodes(nodeIds_opt.map(_.toSet))) + } + } ~ + path("channels") { + formFields(nodeIdFormParam.?) { toRemoteNodeId_opt => + complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + withChannelIdentifier { channel => + complete(eclairApi.channelInfo(channel)) + } + } ~ + path("allchannels") { + complete(eclairApi.allChannels()) + } ~ + path("networkstats") { + complete(eclairApi.networkStats()) + } ~ + path("allupdates") { + formFields(nodeIdFormParam.?) { nodeId_opt => + complete(eclairApi.allUpdates(nodeId_opt)) + } + } ~ + path("findroute") { + formFields(invoiceFormParam, amountMsatFormParam.?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount, 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(nodeIdFormParam, amountMsatFormParam) { (nodeId, amount) => + complete(eclairApi.findRoute(nodeId, amount)) + } + } ~ + path("parseinvoice") { + formFields(invoiceFormParam) { invoice => + complete(invoice) + } + } ~ + path("payinvoice") { + formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => + complete(eclairApi.send(externalId_opt, nodeId, amount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) + case (invoice, Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => + complete(eclairApi.send(externalId_opt, invoice.nodeId, overrideAmount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) + } + } ~ + path("sendtonode") { + formFields(amountMsatFormParam, nodeIdFormParam, paymentHashFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "keysend".as[Boolean].?) { + case (amountMsat, nodeId, Some(paymentHash), maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, keySend) => + keySend match { + case Some(true) => reject(MalformedFormFieldRejection("paymentHash", "You cannot request a KeySend payment and specify a paymentHash")) + case _ => complete(eclairApi.send(externalId_opt, nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) + } + case (amountMsat, nodeId, None, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, keySend) => + keySend match { + case Some(true) => complete(eclairApi.sendWithPreimage(externalId_opt, nodeId, amountMsat, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) + case _ => reject(MalformedFormFieldRejection("paymentHash", "No payment type specified. Either provide a paymentHash or use --keysend=true")) + } + } + } ~ + path("sendtoroute") { + withRoute { hops => + formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "finalCltvExpiry".as[Int], "externalId".?, "parentId".as[UUID].?, "trampolineSecret".as[ByteVector32].?, "trampolineFeesMsat".as[MilliSatoshi].?, "trampolineCltvExpiry".as[Int].?, "trampolineNodes".as[List[PublicKey]](pubkeyListUnmarshaller).?) { + (amountMsat, recipientAmountMsat_opt, invoice, finalCltvExpiry, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) => { + val route = hops match { + case Left(shortChannelIds) => PredefinedChannelRoute(invoice.nodeId, shortChannelIds) + case Right(nodeIds) => PredefinedNodeRoute(nodeIds) } + complete(eclairApi.sendToRoute(amountMsat, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, CltvExpiryDelta(finalCltvExpiry), route, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt.map(CltvExpiryDelta), trampolineNodes_opt.getOrElse(Nil))) } } } + } ~ + path("sendonchain") { + formFields("address".as[String], "amountSatoshis".as[Satoshi], "confirmationTarget".as[Long]) { (address, amount, confirmationTarget) => + complete(eclairApi.sendOnChain(address, amount, confirmationTarget)) + } + } ~ + path("getsentinfo") { + formFields("id".as[UUID]) { id => + complete(eclairApi.sentInfo(Left(id))) + } ~ formFields(paymentHashFormParam) { paymentHash => + complete(eclairApi.sentInfo(Right(paymentHash))) + } + } ~ + path("createinvoice") { + formFields("description".as[String], amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[ByteVector32](sha256HashUnmarshaller).?) { (desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt) => + complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt)) + } + } ~ + path("getinvoice") { + formFields(paymentHashFormParam) { paymentHash => + completeOrNotFound(eclairApi.getInvoice(paymentHash)) + } + } ~ + path("listinvoices") { + formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => + complete(eclairApi.allInvoices(from_opt, to_opt)) + } + } ~ + path("listpendinginvoices") { + formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => + complete(eclairApi.pendingInvoices(from_opt, to_opt)) + } + } ~ + path("getreceivedinfo") { + formFields(paymentHashFormParam) { paymentHash => + completeOrNotFound(eclairApi.receivedInfo(paymentHash)) + } ~ formFields(invoiceFormParam) { invoice => + completeOrNotFound(eclairApi.receivedInfo(invoice.paymentHash)) + } + } ~ + path("audit") { + formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => + complete(eclairApi.audit(from_opt, to_opt)) + } + } ~ + path("networkfees") { + formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => + complete(eclairApi.networkFees(from_opt, to_opt)) + } + } ~ + path("channelstats") { + formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => + complete(eclairApi.channelStats(from_opt, to_opt)) + } + } ~ + path("usablebalances") { + complete(eclairApi.usableBalances()) + } ~ + path("onchainbalance") { + complete(eclairApi.onChainBalance()) + } ~ + path("getnewaddress") { + complete(eclairApi.newAddress()) + } ~ + path("onchaintransactions") { + formFields("count".as[Int].?, "skip".as[Int].?) { (count_opt, skip_opt) => + complete(eclairApi.onChainTransactions(count_opt.getOrElse(10), skip_opt.getOrElse(0))) + } + } ~ + path("signmessage") { + formFields("msg".as[ByteVector](base64DataUnmarshaller)) { message => + complete(eclairApi.signMessage(message)) + } + } ~ + path("verifymessage") { + formFields("msg".as[ByteVector](base64DataUnmarshaller), "sig".as[ByteVector](binaryDataUnmarshaller)) { (message, signature) => + complete(eclairApi.verifyMessage(message, signature)) + } + } + } ~ get { + path("ws") { + handleWebSocketMessages(makeSocketHandler) } } } 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 16e8923ea3..6a9938b085 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 @@ -77,7 +77,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM // no auth Post("/getinfo") ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == Unauthorized) @@ -86,7 +86,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM // wrong auth Post("/getinfo") ~> addCredentials(BasicHttpCredentials("", mockService.password + "what!")) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == Unauthorized) @@ -95,7 +95,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM // correct auth but wrong URL Post("/mistake") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == NotFound) @@ -104,7 +104,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM // wrong param type Post("/channel", FormData(Map("channelId" -> "hey")).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == BadRequest) @@ -115,7 +115,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM // wrong params Post("/connect", FormData("urb" -> "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735").toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == BadRequest) @@ -139,7 +139,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/peers") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -159,7 +159,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/usablebalances") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -187,7 +187,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/getinfo") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -215,7 +215,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -227,7 +227,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/close", FormData("channelId" -> channelIdSerialized).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -239,7 +239,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/close", FormData("channelIds" -> channelIdSerialized, "shortChannelIds" -> "42000x27x3,42000x561x1").toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -259,7 +259,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/connect", FormData("nodeId" -> remoteNodeId.toString()).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -269,7 +269,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/connect", FormData("uri" -> remoteUri.toString).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -287,7 +287,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/payinvoice", FormData("invoice" -> invoice).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == BadRequest) @@ -306,7 +306,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/payinvoice", FormData("invoice" -> invoice).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -315,7 +315,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/payinvoice", FormData("invoice" -> invoice, "amountMsat" -> "123", "feeThresholdSat" -> "112233", "maxFeePct" -> "2.34", "externalId" -> "42").toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -339,7 +339,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/getreceivedinfo", FormData("paymentHash" -> notFound.toHex).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == NotFound) @@ -350,7 +350,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/getreceivedinfo", FormData("paymentHash" -> pending.toHex).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -361,7 +361,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/getreceivedinfo", FormData("paymentHash" -> expired.toHex).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -372,7 +372,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/getreceivedinfo", FormData("paymentHash" -> received.toHex).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -395,7 +395,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/getsentinfo", FormData("id" -> pending.toString).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -406,7 +406,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/getsentinfo", FormData("id" -> failed.toString).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -417,7 +417,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/getsentinfo", FormData("id" -> sent.toString).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -443,7 +443,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/sendtoroute", FormData("nodeIds" -> jsonNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "externalId" -> externalId, "invoice" -> PaymentRequest.write(pr)).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -455,7 +455,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/sendtoroute", FormData("nodeIds" -> csvNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "invoice" -> PaymentRequest.write(pr)).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -477,7 +477,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/networkstats") ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -494,7 +494,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM WS("/ws", wsClient.flow) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> - mockService.route ~> + mockService.finalRoute ~> check { val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty, timestamp = 1553784963659L) val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":1553784963659}""" From 930d20eabd86015bc7fe041bccaeb729cb4bd9f9 Mon Sep 17 00:00:00 2001 From: anton Date: Sat, 19 Dec 2020 18:34:47 +0200 Subject: [PATCH 6/6] Fix tests --- .../src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 45f8668672..ebc46c39f8 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 @@ -209,7 +209,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "100000").toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK) @@ -220,7 +220,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "50000", "feeBaseMsat" -> "100", "feeProportionalMillionths" -> "10").toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> + Route.seal(mockService.finalRoute) ~> check { assert(handled) assert(status == OK)