Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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
}

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
}

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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))
}
Expand All @@ -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))
}
Expand All @@ -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]
}
Expand All @@ -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]
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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]
Expand All @@ -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()
}
Expand Down
Loading