From 0292fb5e604e48b7ed838ee7f8a922124b18ed1d Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 30 May 2019 12:27:46 +0300 Subject: [PATCH 1/3] Correctly decode requests without multipliers --- .../main/scala/fr/acinq/eclair/payment/PaymentRequest.scala | 1 + .../scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala index c1b5894128..0f57fc48a6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala @@ -399,6 +399,7 @@ object PaymentRequest { case a if a.last == 'n' => Some(MilliSatoshi(a.dropRight(1).toLong * 100L)) case a if a.last == 'u' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000L)) case a if a.last == 'm' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000000L)) + case a => Some(MilliSatoshi(a.toLong * 100000000000L)) } def encode(amount: Option[MilliSatoshi]): String = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala index 96a633557c..c2342029fa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala @@ -262,6 +262,12 @@ class PaymentRequestSpec extends FunSuite { assert(PaymentRequest.write(PaymentRequest.read(input.toUpperCase())) == input) } + test("Pay 1 BTC without multiplier") { + val ref = "lnbc11pdkmqhupp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2xvtsv5tc743wgctlza8k3zlpxucl7f3kvjnjptv7xz0nkaww307sdyrvgke2w8kmq7dgz4lkasfn0zvplc9aa4gp8fnhrwfjny0j59sq42x9gp" + val pr = PaymentRequest.read(ref) + assert(pr.amount.contains(MilliSatoshi(100000000000L))) + } + test("nonreg") { val requests = List( "lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl", From b5274d1cd34fc8f4c597ec1dcb628e8f299a8f2c Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 19 Aug 2019 10:55:48 +0300 Subject: [PATCH 2/3] Allow API caller to provide their own UUID for outgoing payments --- .../main/scala/fr/acinq/eclair/Eclair.scala | 8 ++++---- .../scala/fr/acinq/eclair/api/Service.scala | 15 +++++++------- .../eclair/payment/PaymentInitiator.scala | 20 ++++++++++++++----- .../eclair/payment/PaymentLifecycle.scala | 14 ++++++++++--- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 6 +++--- 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index e22b621d35..a122123282 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -78,7 +78,7 @@ trait Eclair { def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]] - def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID] + def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, userProvidedPaymentId_opt: Option[UUID] = None)(implicit timeout: Timeout): Future[UUID] def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] @@ -190,7 +190,7 @@ class EclairImpl(appKit: Kit) extends Eclair { (appKit.paymentInitiator ? SendPaymentToRoute(amount, paymentHash, route, finalCltvExpiry)).mapTo[UUID] } - override def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = { + override def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double], userProvidedPaymentId_opt: Option[UUID])(implicit timeout: Timeout): Future[UUID] = { val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts) val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf) @@ -200,8 +200,8 @@ class EclairImpl(appKit: Kit) extends Eclair { ) val sendPayment = minFinalCltvExpiry_opt match { - case Some(minCltv) => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams)) - case None => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, maxAttempts = maxAttempts, routeParams = Some(routeParams)) + case Some(minCltv) => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams), userProvidedUUID = userProvidedPaymentId_opt) + case None => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, maxAttempts = maxAttempts, routeParams = Some(routeParams), userProvidedUUID = userProvidedPaymentId_opt) } (appKit.paymentInitiator ? sendPayment).mapTo[UUID] } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 7d50d75217..76884f8e11 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -222,17 +222,18 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("payinvoice") { - formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt) => - complete(eclairApi.send(nodeId, amount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) - case (invoice, Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt) => - complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) + formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "userProvidedPaymentId".as[UUID].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, userProvidedPaymentId_opt) => + complete(eclairApi.send(nodeId, amount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, userProvidedPaymentId_opt)) + case (invoice, Some(overrideAmount), maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, userProvidedPaymentId_opt) => + complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, userProvidedPaymentId_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, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?) { (amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt) => - complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) + formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "userProvidedPaymentId".as[UUID].?) { + (amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, userProvidedPaymentId_opt) => + complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt, userProvidedPaymentId_opt = userProvidedPaymentId_opt)) } } ~ path("sendtoroute") { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala index de9bef237a..4a5224b71d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala @@ -17,7 +17,8 @@ package fr.acinq.eclair.payment import java.util.UUID -import akka.actor.{Actor, ActorLogging, ActorRef, Props} + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} import fr.acinq.eclair.NodeParams import fr.acinq.eclair.payment.PaymentLifecycle.GenericSendPayment @@ -28,12 +29,21 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, register: Actor override def receive: Receive = { case c: GenericSendPayment => - val paymentId = UUID.randomUUID() - val payFsm = context.actorOf(PaymentLifecycle.props(nodeParams, paymentId, router, register)) - payFsm forward c - sender ! paymentId + c.userProvidedUUID match { + case Some(uuid) if nodeParams.db.payments.getOutgoingPayment(uuid).isDefined => + sender ! Status.Failure(new RuntimeException(s"User provided paymentId '$uuid' already exists in a database")) + case Some(uuid) => + onSuccess(sender, c, uuid) + case None => + onSuccess(sender, c, UUID.randomUUID()) + } } + def onSuccess(to: ActorRef, c: GenericSendPayment, paymentId: UUID): Unit = { + val payFsm = context.actorOf(PaymentLifecycle.props(nodeParams, paymentId, router, register)) + payFsm forward c + to ! paymentId + } } object PaymentInitiator { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 74374ec9d5..15d7cf20b7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -199,15 +199,23 @@ object PaymentLifecycle { // @formatter:off case class ReceivePayment(amount_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None, paymentPreimage: Option[ByteVector32] = None) - sealed trait GenericSendPayment - case class SendPaymentToRoute(amount: MilliSatoshi, paymentHash: ByteVector32, hops: Seq[PublicKey], finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY) extends GenericSendPayment + sealed trait GenericSendPayment { + val userProvidedUUID: Option[UUID] + } + case class SendPaymentToRoute(amount: MilliSatoshi, + paymentHash: ByteVector32, + hops: Seq[PublicKey], + finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY, + userProvidedUUID: Option[UUID] = None) extends GenericSendPayment + case class SendPayment(amount: MilliSatoshi, paymentHash: ByteVector32, targetNodeId: PublicKey, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY, maxAttempts: Int, - routeParams: Option[RouteParams] = None) extends GenericSendPayment { + routeParams: Option[RouteParams] = None, + userProvidedUUID: Option[UUID] = None) extends GenericSendPayment { require(amount > MilliSatoshi(0), s"amountMsat must be > 0") } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 87fbffeefe..4ee3e8e3ee 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -261,7 +261,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" val eclair = mock[Eclair] - eclair.send(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) + eclair.send(any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) val mockService = new MockService(eclair) Post("/payinvoice", FormData("invoice" -> invoice).toEntity) ~> @@ -270,7 +270,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock check { assert(handled) assert(status == OK) - eclair.send(any, MilliSatoshi(1258000), any, any, any, any, any, any)(any[Timeout]).wasCalled(once) + eclair.send(any, MilliSatoshi(1258000), any, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) } @@ -280,7 +280,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock check { assert(handled) assert(status == OK) - eclair.send(any, MilliSatoshi(123), any, any, any, any, Some(Satoshi(112233)), Some(2.34))(any[Timeout]).wasCalled(once) + eclair.send(any, MilliSatoshi(123), any, any, any, any, Some(Satoshi(112233)), Some(2.34), any)(any[Timeout]).wasCalled(once) } } From 71ed835c66bc4a1d95deb256a9dec97e29d5d04e Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 19 Aug 2019 20:25:47 +0300 Subject: [PATCH 3/3] Add tests --- .../test/scala/fr/acinq/eclair/EclairImplSpec.scala | 12 ++++++++++++ .../scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 10 +++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index 728815f5a3..ebc8c4fc37 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair +import java.util.UUID + import akka.actor.ActorSystem import akka.testkit.{TestKit, TestProbe} import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} @@ -35,6 +37,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate import org.mockito.scalatest.IdiomaticMockito + import scala.concurrent.Await import scala.util.{Failure, Success} import scala.concurrent.duration._ @@ -126,6 +129,15 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu assert(send3.paymentHash == ByteVector32.Zeroes) assert(send3.routeParams.get.maxFeeBase == Satoshi(123).toMilliSatoshi) // conversion sat -> msat assert(send3.routeParams.get.maxFeePct == 4.20) + + // with user defined UUID + val uuid = UUID.randomUUID() + eclair.send(recipientNodeId = nodeId, amount = MilliSatoshi(123), paymentHash = ByteVector32.Zeroes, minFinalCltvExpiry_opt = None, userProvidedPaymentId_opt = Some(uuid)) + val send4 = paymentInitiator.expectMsgType[SendPayment] + assert(send4.targetNodeId == nodeId) + assert(send4.amount == MilliSatoshi(123)) + assert(send4.paymentHash == ByteVector32.Zeroes) + assert(send4.userProvidedUUID.contains(uuid)) } test("allupdates can filter by nodeId") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 4ee3e8e3ee..942c6f9441 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -273,7 +273,6 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock eclair.send(any, MilliSatoshi(1258000), any, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) } - Post("/payinvoice", FormData("invoice" -> invoice, "amountMsat" -> "123", "feeThresholdSat" -> "112233", "maxFeePct" -> "2.34").toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> Route.seal(mockService.route) ~> @@ -283,6 +282,15 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock eclair.send(any, MilliSatoshi(123), any, any, any, any, Some(Satoshi(112233)), Some(2.34), any)(any[Timeout]).wasCalled(once) } + val uuid = UUID.randomUUID() + Post("/payinvoice", FormData("invoice" -> invoice, "amountMsat" -> "123", "feeThresholdSat" -> "112233", "maxFeePct" -> "2.34", "userProvidedPaymentId" -> uuid.toString).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + eclair.send(any, MilliSatoshi(123), any, any, any, any, Some(Satoshi(112233)), Some(2.34), Some(uuid))(any[Timeout]).wasCalled(once) + } } test("'getreceivedinfo' method should respond HTTP 404 with a JSON encoded response if the element is not found") {