From 5fdaf7bfb90c5d4129e1b725ab8bf2fc9557e5a9 Mon Sep 17 00:00:00 2001 From: anton Date: Sun, 14 Apr 2019 10:54:17 +0300 Subject: [PATCH 1/3] Add LNUrl tag --- .../fr/acinq/eclair/payment/PaymentRequest.scala | 16 ++++++++++++---- .../eclair/payment/PaymentRequestSpec.scala | 13 +++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) 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 994fdbafe1..651b175bcb 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 @@ -115,8 +115,8 @@ object PaymentRequest { Block.LivenetGenesisBlock.hash -> "lnbc") def apply(chainHash: ByteVector32, amount: Option[MilliSatoshi], paymentHash: ByteVector32, privateKey: PrivateKey, - description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None, - extraHops: List[List[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L): PaymentRequest = { + description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, + timestamp: Long = System.currentTimeMillis() / 1000L, lnUrl: Option[String] = None): PaymentRequest = { val prefix = prefixes(chainHash) @@ -129,7 +129,8 @@ object PaymentRequest { Some(PaymentHash(paymentHash)), Some(Description(description)), fallbackAddress.map(FallbackAddress(_)), - expirySeconds.map(Expiry(_)) + expirySeconds.map(Expiry(_)), + lnUrl.map(LNUrl(_)) ).flatten ++ extraHops.map(RoutingInfo(_)), signature = ByteVector.empty) .sign(privateKey) @@ -184,6 +185,13 @@ object PaymentRequest { */ case class Description(description: String) extends TaggedField + /** + * LNUrl + * + * @param request an embedded query string for advanced interactions + */ + case class LNUrl(request: String) extends TaggedField + /** * Hash * @@ -341,7 +349,7 @@ object PaymentRequest { .typecase(9, dataCodec(ubyte(5) :: alignedBytesCodec(bytes)).as[FallbackAddress]) .typecase(10, dataCodec(bits).as[UnknownTag10]) .typecase(11, dataCodec(bits).as[UnknownTag11]) - .typecase(12, dataCodec(bits).as[UnknownTag12]) + .typecase(12, dataCodec(alignedBytesCodec(utf8)).as[LNUrl]) .typecase(13, dataCodec(alignedBytesCodec(utf8)).as[Description]) .typecase(14, dataCodec(bits).as[UnknownTag14]) .typecase(15, dataCodec(bits).as[UnknownTag15]) 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 9e4f93c7e8..94776d8732 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 @@ -314,4 +314,17 @@ class PaymentRequestSpec extends FunSuite { for (req <- requests) { assert(PaymentRequest.write(PaymentRequest.read(req)) == req) } } + + test("embedded lnurl is detected") { + val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(MilliSatoshi(500000L)), paymentHash = ByteVector32(ByteVector.fromValidHex("00" * 32)), + priv, "Invoice with embedded lnurl", fallbackAddress = None, expirySeconds = None, extraHops = Nil, lnUrl = Some("https://lnurl.serice.com?tag=multipart")) + val serialized = PaymentRequest.write(pr) + val pr1 = PaymentRequest.read(serialized) + assert(pr1.prefix == "lnbc") + assert(pr1.amount == Some(MilliSatoshi(500000L))) + assert(pr1.paymentHash == ByteVector32(ByteVector.fromValidHex("00" * 32))) + assert(pr1.nodeId == pub) + assert(pr1.description == Left("Invoice with embedded lnurl")) + assert(pr.tags.collectFirst { case lnurl: PaymentRequest.LNUrl => lnurl.request }.contains("https://lnurl.serice.com?tag=multipart")) + } } From bad85fa143718e258bc9860a45ad94204ec583b2 Mon Sep 17 00:00:00 2001 From: anton Date: Sun, 14 Apr 2019 12:01:10 +0300 Subject: [PATCH 2/3] Generate multiple payment requests --- .../eclair/payment/LocalPaymentHandler.scala | 29 +++++--- .../eclair/payment/PaymentLifecycle.scala | 6 +- .../eclair/integration/IntegrationSpec.scala | 45 ++++++------ .../eclair/payment/PaymentHandlerSpec.scala | 69 +++++++++++-------- .../scala/fr/acinq/eclair/gui/Handlers.scala | 3 +- 5 files changed, 88 insertions(+), 64 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 2fce5d5cb6..5833787b87 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -50,18 +50,28 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin case PurgeExpiredRequests => context.become(run(hash2preimage.filterNot { case (_, pr) => hasExpired(pr) })) - case ReceivePayment(amount_opt, desc, expirySeconds_opt, extraHops, fallbackAddress_opt) => + case ReceivePayment(amount_opt, desc, expirySeconds_opt, extraHops, fallbackAddress_opt, lnurl_opt, quantity) => + Try { if (hash2preimage.size > nodeParams.maxPendingPaymentRequests) { throw new RuntimeException(s"too many pending payment requests (max=${nodeParams.maxPendingPaymentRequests})") } - val paymentPreimage = randomBytes32 - val paymentHash = Crypto.sha256(paymentPreimage) - val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds) - val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops) - log.debug(s"generated payment request=${PaymentRequest.write(paymentRequest)} from amount=$amount_opt") - sender ! paymentRequest - context.become(run(hash2preimage + (paymentHash -> PendingPaymentRequest(paymentPreimage, paymentRequest)))) + + val invoices = for (n <- 1 to quantity) yield { + val paymentPreimage = randomBytes32 + val paymentHash = Crypto.sha256(paymentPreimage) + val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds) + + val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, + desc, fallbackAddress = None, expirySeconds = Some(expirySeconds), extraHops = extraHops, lnUrl = lnurl_opt) + + log.debug(s"generated payment request=${PaymentRequest.write(paymentRequest)} from amount=$amount_opt") + PendingPaymentRequest(paymentPreimage, paymentRequest) + } + + sender ! GeneratedPaymentRequests(invoices.toList.map(_.paymentRequest)) + val invoices1 = for (ppr @ PendingPaymentRequest(_, paymentRequest) <- invoices) yield paymentRequest.paymentHash -> ppr + context.become(run(hash2preimage ++ invoices1.toMap)) } recover { case t => sender ! Status.Failure(t) } case CheckPayment(paymentHash) => @@ -116,9 +126,10 @@ object LocalPaymentHandler { case class PendingPaymentRequest(preimage: ByteVector32, paymentRequest: PaymentRequest) + case class GeneratedPaymentRequests(requests: List[PaymentRequest]) + def hasExpired(pr: PendingPaymentRequest): Boolean = pr.paymentRequest.expiry match { case Some(expiry) => pr.paymentRequest.timestamp + expiry <= Platform.currentTime / 1000 case None => false // this request will never expire } - } \ No newline at end of file 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 2449ecf168..32e12c1336 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 @@ -180,10 +180,8 @@ object PaymentLifecycle { def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], sourceNodeId, router, register) // @formatter:off - case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None) - /** - * @param maxFeePct set by default to 3% as a safety measure (even if a route is found, if fee is higher than that payment won't be attempted) - */ + case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None, lnUrl: Option[String] = None, quantity: Int = 1) + case class SendPayment(amountMsat: Long, paymentHash: ByteVector32, targetNodeId: PublicKey, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 142f74a333..1ffc5b8e39 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -34,6 +34,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx.ErrorPacket import fr.acinq.eclair.io.Peer.{Disconnect, PeerRoutingMessage} import fr.acinq.eclair.io.{NodeURI, Peer} +import fr.acinq.eclair.payment.LocalPaymentHandler.GeneratedPaymentRequests import fr.acinq.eclair.payment.PaymentLifecycle.{State => _, _} import fr.acinq.eclair.payment.{LocalPaymentHandler, PaymentRequest} import fr.acinq.eclair.router.Graph.WeightRatios @@ -258,10 +259,10 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val amountMsat = MilliSatoshi(4200000) // first we retrieve a payment hash from D sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] + val gpr = sender.expectMsgType[GeneratedPaymentRequests] // then we make the actual payment sender.send(nodes("A").paymentInitiator, - SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams)) + SendPayment(amountMsat.amount, gpr.requests.head.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams)) sender.expectMsgType[PaymentSucceeded] } @@ -281,9 +282,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // we retrieve a payment hash from D val amountMsat = MilliSatoshi(4200000) sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] + val gpr = sender.expectMsgType[GeneratedPaymentRequests] // then we make the actual payment, do not randomize the route to make sure we route through node B - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) + val sendReq = SendPayment(amountMsat.amount, gpr.requests.head.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) // A will receive an error from B that include the updated channel update, then will retry the payment sender.expectMsgType[PaymentSucceeded](5 seconds) @@ -317,9 +318,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // first we retrieve a payment hash from D val amountMsat = MilliSatoshi(300000000L) sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] + val gpr = sender.expectMsgType[GeneratedPaymentRequests] // then we make the payment (B-C has a smaller capacity than A-B and C-D) - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) + val sendReq = SendPayment(amountMsat.amount, gpr.requests.head.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an error from C, then retry and route around C: A->B->E->C->D sender.expectMsgType[PaymentSucceeded](5 seconds) @@ -342,15 +343,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // first we retrieve a payment hash from D for 2 mBTC val amountMsat = MilliSatoshi(200000000L) sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] + val gpr = sender.expectMsgType[GeneratedPaymentRequests] // A send payment of only 1 mBTC - val sendReq = SendPayment(100000000L, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) + val sendReq = SendPayment(100000000L, gpr.requests.head.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D val failed = sender.expectMsgType[PaymentFailed] - assert(failed.paymentHash === pr.paymentHash) + assert(failed.paymentHash === gpr.requests.head.paymentHash) assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, IncorrectPaymentAmount)) } @@ -360,15 +361,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // first we retrieve a payment hash from D for 2 mBTC val amountMsat = MilliSatoshi(200000000L) sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] + val gpr = sender.expectMsgType[GeneratedPaymentRequests] // A send payment of 6 mBTC - val sendReq = SendPayment(600000000L, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) + val sendReq = SendPayment(600000000L, gpr.requests.head.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D val failed = sender.expectMsgType[PaymentFailed] - assert(failed.paymentHash === pr.paymentHash) + assert(failed.paymentHash === gpr.requests.head.paymentHash) assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, IncorrectPaymentAmount)) } @@ -378,10 +379,10 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // first we retrieve a payment hash from D for 2 mBTC val amountMsat = MilliSatoshi(200000000L) sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] + val gpr = sender.expectMsgType[GeneratedPaymentRequests] // A send payment of 3 mBTC, more than asked but it should still be accepted - val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) + val sendReq = SendPayment(300000000L, gpr.requests.head.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[PaymentSucceeded] } @@ -392,9 +393,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService for (_ <- 0 until 7) { val amountMsat = MilliSatoshi(1000000000L) sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 payment")) - val pr = sender.expectMsgType[PaymentRequest] + val gpr = sender.expectMsgType[GeneratedPaymentRequests] - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) + val sendReq = SendPayment(amountMsat.amount, gpr.requests.head.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[PaymentSucceeded] } @@ -405,11 +406,11 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // first we retrieve a payment hash from C val amountMsat = MilliSatoshi(2000) sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amountMsat), "Change from coffee")) - val pr = sender.expectMsgType[PaymentRequest](30 seconds) + val gpr = sender.expectMsgType[GeneratedPaymentRequests](30 seconds) // the payment is requesting to use a capacity-optimized route which will select node G even though it's a bit more expensive sender.send(nodes("A").paymentInitiator, - SendPayment(amountMsat.amount, pr.paymentHash, nodes("C").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams.map(_.copy(ratios = Some(WeightRatios(0, 0, 1)))))) + SendPayment(amountMsat.amount, gpr.requests.head.paymentHash, nodes("C").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams.map(_.copy(ratios = Some(WeightRatios(0, 0, 1)))))) awaitCond({ sender.expectMsgType[PaymentResult](10 seconds) match { @@ -736,8 +737,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // first we send 3 mBTC to F so that it has a balance val amountMsat = MilliSatoshi(300000000L) sender.send(paymentHandlerF, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] - val sendReq = SendPayment(300000000L, pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams) + val gpr = sender.expectMsgType[GeneratedPaymentRequests] + val sendReq = SendPayment(300000000L, gpr.requests.head.paymentHash, gpr.requests.head.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) // we forward the htlc to the payment handler forwardHandlerF.expectMsgType[UpdateAddHtlc] @@ -749,8 +750,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // we now send a few htlcs C->F and F->C in order to obtain a commitments with multiple htlcs def send(amountMsat: Long, paymentHandler: ActorRef, paymentInitiator: ActorRef) = { sender.send(paymentHandler, ReceivePayment(Some(MilliSatoshi(amountMsat)), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] - val sendReq = SendPayment(amountMsat, pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams) + val gpr = sender.expectMsgType[GeneratedPaymentRequests] + val sendReq = SendPayment(amountMsat, gpr.requests.head.paymentHash, gpr.requests.head.nodeId, routeParams = integrationTestRouteParams) sender.send(paymentInitiator, sendReq) sender.expectNoMsg() } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 2c842f6907..8aa1d6e686 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -22,7 +22,7 @@ import akka.testkit.{TestActorRef, TestKit, TestProbe} import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} -import fr.acinq.eclair.payment.LocalPaymentHandler.PendingPaymentRequest +import fr.acinq.eclair.payment.LocalPaymentHandler.{GeneratedPaymentRequests, PendingPaymentRequest} import fr.acinq.eclair.payment.PaymentLifecycle.{CheckPayment, ReceivePayment} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UnknownPaymentHash, UpdateAddHtlc} @@ -50,67 +50,80 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] - sender.send(handler, CheckPayment(pr.paymentHash)) + val gpr = sender.expectMsgType[GeneratedPaymentRequests] + sender.send(handler, CheckPayment(gpr.requests.head.paymentHash)) assert(sender.expectMsgType[Boolean] === false) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, gpr.requests.head.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) - sender.send(handler, CheckPayment(pr.paymentHash)) + sender.send(handler, CheckPayment(gpr.requests.head.paymentHash)) assert(sender.expectMsgType[Boolean] === true) } { sender.send(handler, ReceivePayment(Some(amountMsat), "another coffee")) - val pr = sender.expectMsgType[PaymentRequest] - sender.send(handler, CheckPayment(pr.paymentHash)) + val gpr = sender.expectMsgType[GeneratedPaymentRequests] + sender.send(handler, CheckPayment(gpr.requests.head.paymentHash)) assert(sender.expectMsgType[Boolean] === false) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, gpr.requests.head.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) - sender.send(handler, CheckPayment(pr.paymentHash)) + sender.send(handler, CheckPayment(gpr.requests.head.paymentHash)) assert(sender.expectMsgType[Boolean] === true) } { sender.send(handler, ReceivePayment(Some(amountMsat), "bad expiry")) - val pr = sender.expectMsgType[PaymentRequest] - sender.send(handler, CheckPayment(pr.paymentHash)) + val gpr = sender.expectMsgType[GeneratedPaymentRequests] + sender.send(handler, CheckPayment(gpr.requests.head.paymentHash)) assert(sender.expectMsgType[Boolean] === false) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, gpr.requests.head.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, ByteVector.empty) sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(FinalExpiryTooSoon)) eventListener.expectNoMsg(300 milliseconds) - sender.send(handler, CheckPayment(pr.paymentHash)) + sender.send(handler, CheckPayment(gpr.requests.head.paymentHash)) assert(sender.expectMsgType[Boolean] === false) } + { sender.send(handler, ReceivePayment(Some(amountMsat), "timeout expired", Some(1L))) //allow request to timeout Thread.sleep(1001) - val pr = sender.expectMsgType[PaymentRequest] - sender.send(handler, CheckPayment(pr.paymentHash)) + val gpr = sender.expectMsgType[GeneratedPaymentRequests] + sender.send(handler, CheckPayment(gpr.requests.head.paymentHash)) assert(sender.expectMsgType[Boolean] === false) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, gpr.requests.head.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(UnknownPaymentHash)) // We chose UnknownPaymentHash on purpose. So if you have expired by 1 second or 1 hour you get the same error message. eventListener.expectNoMsg(300 milliseconds) - sender.send(handler, CheckPayment(pr.paymentHash)) + sender.send(handler, CheckPayment(gpr.requests.head.paymentHash)) assert(sender.expectMsgType[Boolean] === false) // make sure that the request is indeed pruned sender.send(handler, 'requests) - sender.expectMsgType[Map[ByteVector, PendingPaymentRequest]].contains(pr.paymentHash) + sender.expectMsgType[Map[ByteVector, PendingPaymentRequest]].contains(gpr.requests.head.paymentHash) sender.send(handler, LocalPaymentHandler.PurgeExpiredRequests) awaitCond({ sender.send(handler, 'requests) - sender.expectMsgType[Map[ByteVector32, PendingPaymentRequest]].contains(pr.paymentHash) == false + !sender.expectMsgType[Map[ByteVector32, PendingPaymentRequest]].contains(gpr.requests.head.paymentHash) }) } + + { + val paymentGroupId = "payment-group-id-1" + sender.send(handler, ReceivePayment(Some(MilliSatoshi(4000000000L)), s"1 coffee in a single payment #$paymentGroupId", lnUrl = Some("https://service.com/request1?tag=multipart"))) + val gpr = sender.expectMsgType[GeneratedPaymentRequests] + sender.send(handler, ReceivePayment(None, s"same 1 coffee in 10 sub-payments #$paymentGroupId", quantity = 10)) + val gpr1 = sender.expectMsgType[GeneratedPaymentRequests] + assert(gpr1.requests.forall(_.amount.isEmpty)) // Additional invoices are amountless so payment can spread them across channels + assert(gpr1.requests.map(_.paymentHash).distinct.size == gpr1.requests.size) // Additional invoices have unique payment hashes + assert((gpr.requests ++ gpr1.requests).forall(_.description.left.get.contains(paymentGroupId))) // All invoices contain a reference to the same group id + assert(gpr1.requests.size == 10) + } } test("Payment request generation should fail when the amount asked in not valid") { @@ -137,8 +150,8 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike // success with 1 mBTC sender.send(handler, ReceivePayment(Some(MilliSatoshi(100000000L)), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.amount.contains(MilliSatoshi(100000000L)) && pr.nodeId.toString == nodeParams.nodeId.toString) + val gpr = sender.expectMsgType[GeneratedPaymentRequests] + assert(gpr.requests.head.amount.contains(MilliSatoshi(100000000L)) && gpr.requests.head.nodeId.toString == nodeParams.nodeId.toString) } test("Payment request generation should fail when there are too many pending requests") { @@ -148,7 +161,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike for (i <- 0 to nodeParams.maxPendingPaymentRequests) { sender.send(handler, ReceivePayment(None, s"Request #$i")) - sender.expectMsgType[PaymentRequest] + sender.expectMsgType[GeneratedPaymentRequests] } // over limit @@ -161,8 +174,8 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val sender = TestProbe() sender.send(handler, ReceivePayment(None, "This is a donation PR")) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.amount.isEmpty && pr.nodeId.toString == Alice.nodeParams.nodeId.toString) + val gpr = sender.expectMsgType[GeneratedPaymentRequests] + assert(gpr.requests.head.amount.isEmpty && gpr.requests.head.nodeId.toString == Alice.nodeParams.nodeId.toString) } test("Payment request generation should handle custom expiries or use the default otherwise") { @@ -170,10 +183,10 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val sender = TestProbe() sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee")) - assert(sender.expectMsgType[PaymentRequest].expiry === Some(Alice.nodeParams.paymentRequestExpiry.toSeconds)) + assert(sender.expectMsgType[GeneratedPaymentRequests].requests.head.expiry === Some(Alice.nodeParams.paymentRequestExpiry.toSeconds)) sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee with custom expiry", expirySeconds_opt = Some(60))) - assert(sender.expectMsgType[PaymentRequest].expiry === Some(60)) + assert(sender.expectMsgType[GeneratedPaymentRequests].requests.head.expiry === Some(60)) } test("Generated payment request contains the provided extra hops") { @@ -189,9 +202,9 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val route_x_t = extraHop_x_t :: Nil sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee with additional routing info", extraHops = List(route_x_z, route_x_t))) - assert(sender.expectMsgType[PaymentRequest].routingInfo === Seq(route_x_z, route_x_t)) + assert(sender.expectMsgType[GeneratedPaymentRequests].requests.head.routingInfo === Seq(route_x_z, route_x_t)) sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee without routing info")) - assert(sender.expectMsgType[PaymentRequest].routingInfo === Nil) + assert(sender.expectMsgType[GeneratedPaymentRequests].requests.head.routingInfo === Nil) } } diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala index f3d55232fb..2397002bea 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala @@ -22,6 +22,7 @@ import fr.acinq.bitcoin.MilliSatoshi import fr.acinq.eclair._ import fr.acinq.eclair.gui.controllers._ import fr.acinq.eclair.io.{NodeURI, Peer} +import fr.acinq.eclair.payment.LocalPaymentHandler.GeneratedPaymentRequests import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentResult, ReceivePayment, SendPayment} import fr.acinq.eclair.payment._ import grizzled.slf4j.Logging @@ -102,7 +103,7 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte def receive(amountMsat_opt: Option[MilliSatoshi], description: String): Future[String] = for { kit <- fKit - res <- (kit.paymentHandler ? ReceivePayment(amountMsat_opt, description)).mapTo[PaymentRequest].map(PaymentRequest.write) + res <- (kit.paymentHandler ? ReceivePayment(amountMsat_opt, description)).mapTo[GeneratedPaymentRequests].map(PaymentRequest write _.requests.head) } yield res /** From b89e5cf9151c5618c655d0b6a10347aee24571e9 Mon Sep 17 00:00:00 2001 From: anton Date: Sun, 14 Apr 2019 14:20:15 +0300 Subject: [PATCH 3/3] Fix fuzzy tests --- .../src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 508b032f02..812f90b44e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.payment.LocalPaymentHandler.GeneratedPaymentRequests import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Hop @@ -108,8 +109,8 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi override def receive: Receive = ??? def waitingForPaymentRequest: Receive = { - case req: PaymentRequest => - channel ! buildCmdAdd(req.paymentHash, req.nodeId) + case gpr: GeneratedPaymentRequests => + channel ! buildCmdAdd(gpr.requests.head.paymentHash, gpr.requests.head.nodeId) context become waitingForFulfill(false) }