@@ -20,17 +20,21 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
2020import akka .actor .typed .{ActorRef , Behavior }
2121import fr .acinq .bitcoin .scalacompat .Crypto .PrivateKey
2222import fr .acinq .bitcoin .scalacompat .{ByteVector32 , Crypto }
23+ import fr .acinq .eclair .EncodedNodeId .ShortChannelIdDir
2324import fr .acinq .eclair .crypto .Sphinx .RouteBlinding
2425import fr .acinq .eclair .db .{IncomingBlindedPayment , IncomingPaymentStatus , PaymentType }
2526import fr .acinq .eclair .message .{OnionMessages , Postman }
26- import fr .acinq .eclair .payment .MinimalBolt12Invoice
2727import fr .acinq .eclair .payment .offer .OfferPaymentMetadata .MinimalInvoiceData
2828import fr .acinq .eclair .payment .receive .MultiPartHandler
2929import fr .acinq .eclair .payment .receive .MultiPartHandler .{CreateInvoiceActor , ReceivingRoute }
30+ import fr .acinq .eclair .payment .relay .Relayer .RelayFees
31+ import fr .acinq .eclair .payment .{Bolt12Invoice , MinimalBolt12Invoice }
32+ import fr .acinq .eclair .router .BlindedRouteCreation .aggregatePaymentInfo
33+ import fr .acinq .eclair .router .Router
3034import fr .acinq .eclair .wire .protocol .OfferTypes .{InvoiceRequest , InvoiceTlv , Offer }
3135import fr .acinq .eclair .wire .protocol .PaymentOnion .FinalPayload
3236import fr .acinq .eclair .wire .protocol ._
33- import fr .acinq .eclair .{Logs , MilliSatoshi , NodeParams , TimestampMilli , TimestampSecond , randomBytes32 }
37+ import fr .acinq .eclair .{CltvExpiryDelta , Logs , MilliSatoshi , MilliSatoshiLong , NodeParams , TimestampMilli , TimestampSecond , nodeFee , randomBytes32 }
3438import scodec .bits .ByteVector
3539
3640import scala .concurrent .duration .FiniteDuration
@@ -61,7 +65,7 @@ object OfferManager {
6165
6266 case class RequestInvoice (messagePayload : MessageOnion .InvoiceRequestPayload , blindedKey : PrivateKey , postman : ActorRef [Postman .SendMessage ]) extends Command
6367
64- case class ReceivePayment (replyTo : ActorRef [MultiPartHandler .GetIncomingPaymentActor .Command ], paymentHash : ByteVector32 , payload : FinalPayload .Blinded ) extends Command
68+ case class ReceivePayment (replyTo : ActorRef [MultiPartHandler .GetIncomingPaymentActor .Command ], paymentHash : ByteVector32 , payload : FinalPayload .Blinded , amountReceived : MilliSatoshi ) extends Command
6569
6670 /**
6771 * Offer handlers must be implemented in separate plugins and respond to these two `HandlerCommand`.
@@ -89,15 +93,15 @@ object OfferManager {
8993
9094 private case class RegisteredOffer (offer : Offer , nodeKey : Option [PrivateKey ], pathId_opt : Option [ByteVector32 ], handler : ActorRef [HandlerCommand ])
9195
92- def apply (nodeParams : NodeParams , router : akka.actor. ActorRef , paymentTimeout : FiniteDuration ): Behavior [Command ] = {
96+ def apply (nodeParams : NodeParams , paymentTimeout : FiniteDuration ): Behavior [Command ] = {
9397 Behaviors .setup { context =>
9498 Behaviors .withMdc(Logs .mdc(category_opt = Some (Logs .LogCategory .PAYMENT ))) {
95- new OfferManager (nodeParams, router, paymentTimeout, context).normal(Map .empty)
99+ new OfferManager (nodeParams, paymentTimeout, context).normal(Map .empty)
96100 }
97101 }
98102 }
99103
100- private class OfferManager (nodeParams : NodeParams , router : akka.actor. ActorRef , paymentTimeout : FiniteDuration , context : ActorContext [Command ]) {
104+ private class OfferManager (nodeParams : NodeParams , paymentTimeout : FiniteDuration , context : ActorContext [Command ]) {
101105 def normal (registeredOffers : Map [ByteVector32 , RegisteredOffer ]): Behavior [Command ] = {
102106 Behaviors .receiveMessage {
103107 case RegisterOffer (offer, nodeKey, pathId_opt, handler) =>
@@ -108,19 +112,19 @@ object OfferManager {
108112 registeredOffers.get(messagePayload.invoiceRequest.offer.offerId) match {
109113 case Some (registered) if registered.pathId_opt.map(_.bytes) == messagePayload.pathId_opt && messagePayload.invoiceRequest.isValid =>
110114 context.log.debug(" received valid invoice request for offerId={}" , messagePayload.invoiceRequest.offer.offerId)
111- val child = context.spawnAnonymous(InvoiceRequestActor (nodeParams, messagePayload.invoiceRequest, registered.handler, registered.nodeKey.getOrElse(blindedKey), router, messagePayload.replyPath, postman))
115+ val child = context.spawnAnonymous(InvoiceRequestActor (nodeParams, messagePayload.invoiceRequest, registered.handler, registered.nodeKey.getOrElse(blindedKey), messagePayload.replyPath, postman))
112116 child ! InvoiceRequestActor .RequestInvoice
113117 case _ => context.log.debug(" offer {} is not registered or invoice request is invalid" , messagePayload.invoiceRequest.offer.offerId)
114118 }
115119 Behaviors .same
116- case ReceivePayment (replyTo, paymentHash, payload) =>
120+ case ReceivePayment (replyTo, paymentHash, payload, amountReceived ) =>
117121 MinimalInvoiceData .decode(payload.pathId) match {
118122 case Some (signed) =>
119123 registeredOffers.get(signed.offerId) match {
120124 case Some (RegisteredOffer (offer, _, _, handler)) =>
121125 MinimalInvoiceData .verify(nodeParams.nodeId, signed) match {
122126 case Some (metadata) if Crypto .sha256(metadata.preimage) == paymentHash =>
123- val child = context.spawnAnonymous(PaymentActor (nodeParams, replyTo, offer, metadata, paymentTimeout))
127+ val child = context.spawnAnonymous(PaymentActor (nodeParams, replyTo, offer, metadata, amountReceived, paymentTimeout))
124128 handler ! HandlePayment (child, signed.offerId, metadata.pluginData_opt)
125129 case Some (_) => replyTo ! MultiPartHandler .GetIncomingPaymentActor .RejectPayment (s " preimage does not match payment hash for offer ${signed.offerId.toHex}" )
126130 case None => replyTo ! MultiPartHandler .GetIncomingPaymentActor .RejectPayment (s " invalid signature for metadata for offer ${signed.offerId.toHex}" )
@@ -150,33 +154,53 @@ object OfferManager {
150154 * @param customTlvs custom TLVs to add to the invoice.
151155 */
152156 case class ApproveRequest (amount : MilliSatoshi ,
153- routes : Seq [ReceivingRoute ],
157+ routes : Seq [Route ],
154158 pluginData_opt : Option [ByteVector ] = None ,
155159 additionalTlvs : Set [InvoiceTlv ] = Set .empty,
156160 customTlvs : Set [GenericTlv ] = Set .empty) extends Command
157161
162+ /**
163+ * @param recipientPaysFees If true, fees for the blinded route will be hidden to the payer and paid by the recipient.
164+ */
165+ case class Route (hops : Seq [Router .ChannelHop ], recipientPaysFees : Boolean , maxFinalExpiryDelta : CltvExpiryDelta , shortChannelIdDir_opt : Option [ShortChannelIdDir ] = None ) {
166+ def finalize (nodePriv : PrivateKey , preimage : ByteVector32 , amount : MilliSatoshi , invoiceRequest : InvoiceRequest , minFinalExpiryDelta : CltvExpiryDelta , pluginData_opt : Option [ByteVector ]): ReceivingRoute = {
167+ val (paymentInfo, metadata) = if (recipientPaysFees) {
168+ val realPaymentInfo = aggregatePaymentInfo(amount, hops, minFinalExpiryDelta)
169+ val recipientFees = RelayFees (realPaymentInfo.feeBase, realPaymentInfo.feeProportionalMillionths)
170+ val metadata = MinimalInvoiceData (preimage, invoiceRequest.payerId, TimestampSecond .now(), invoiceRequest.quantity, amount, recipientFees, pluginData_opt)
171+ val paymentInfo = realPaymentInfo.copy(feeBase = 0 msat, feeProportionalMillionths = 0 )
172+ (paymentInfo, metadata)
173+ } else {
174+ val paymentInfo = aggregatePaymentInfo(amount, hops, minFinalExpiryDelta)
175+ val metadata = MinimalInvoiceData (preimage, invoiceRequest.payerId, TimestampSecond .now(), invoiceRequest.quantity, amount, RelayFees .zero, pluginData_opt)
176+ (paymentInfo, metadata)
177+ }
178+ val pathId = MinimalInvoiceData .encode(nodePriv, invoiceRequest.offer.offerId, metadata)
179+ ReceivingRoute (hops, pathId, maxFinalExpiryDelta, paymentInfo, shortChannelIdDir_opt)
180+ }
181+ }
182+
158183 /**
159184 * Sent by the offer handler to reject the request. For instance because stock has been exhausted.
160185 */
161186 case class RejectRequest (message : String ) extends Command
162187
163- private case class WrappedInvoiceResponse (response : CreateInvoiceActor . Bolt12InvoiceResponse ) extends Command
188+ private case class WrappedInvoiceResponse (invoice : Bolt12Invoice ) extends Command
164189
165190 private case class WrappedOnionMessageResponse (response : Postman .OnionMessageResponse ) extends Command
166191
167192 def apply (nodeParams : NodeParams ,
168193 invoiceRequest : InvoiceRequest ,
169194 offerHandler : ActorRef [HandleInvoiceRequest ],
170195 nodeKey : PrivateKey ,
171- router : akka.actor.ActorRef ,
172196 pathToSender : RouteBlinding .BlindedRoute ,
173197 postman : ActorRef [Postman .SendMessage ]): Behavior [Command ] = {
174198 Behaviors .setup { context =>
175199 Behaviors .withMdc(Logs .mdc(category_opt = Some (Logs .LogCategory .PAYMENT ))) {
176200 Behaviors .receiveMessagePartial {
177201 case RequestInvoice =>
178202 offerHandler ! HandleInvoiceRequest (context.self, invoiceRequest)
179- new InvoiceRequestActor (nodeParams, invoiceRequest, nodeKey, router, pathToSender, postman, context).waitForHandler()
203+ new InvoiceRequestActor (nodeParams, invoiceRequest, nodeKey, pathToSender, postman, context).waitForHandler()
180204 }
181205 }
182206 }
@@ -185,7 +209,6 @@ object OfferManager {
185209 private class InvoiceRequestActor (nodeParams : NodeParams ,
186210 invoiceRequest : InvoiceRequest ,
187211 nodeKey : PrivateKey ,
188- router : akka.actor.ActorRef ,
189212 pathToSender : RouteBlinding .BlindedRoute ,
190213 postman : ActorRef [Postman .SendMessage ],
191214 context : ActorContext [Command ]) {
@@ -197,9 +220,8 @@ object OfferManager {
197220 waitForSent()
198221 case ApproveRequest (amount, routes, pluginData_opt, additionalTlvs, customTlvs) =>
199222 val preimage = randomBytes32()
200- val metadata = MinimalInvoiceData (preimage, invoiceRequest.payerId, TimestampSecond .now(), invoiceRequest.quantity, amount, pluginData_opt)
201- val pathId = MinimalInvoiceData .encode(nodeParams.privateKey, invoiceRequest.offer.offerId, metadata)
202- val receivePayment = MultiPartHandler .ReceiveOfferPayment (context.messageAdapter[CreateInvoiceActor .Bolt12InvoiceResponse ](WrappedInvoiceResponse ), nodeKey, invoiceRequest, routes, router, preimage, pathId, additionalTlvs, customTlvs)
223+ val receivingRoutes = routes.map(_.finalize(nodeParams.privateKey, preimage, amount, invoiceRequest, nodeParams.channelConf.minFinalExpiryDelta, pluginData_opt))
224+ val receivePayment = MultiPartHandler .ReceiveOfferPayment (context.messageAdapter[Bolt12Invoice ](WrappedInvoiceResponse ), nodeKey, invoiceRequest, receivingRoutes, preimage, additionalTlvs, customTlvs)
203225 val child = context.spawnAnonymous(CreateInvoiceActor (nodeParams))
204226 child ! CreateInvoiceActor .CreateBolt12Invoice (receivePayment)
205227 waitForInvoice()
@@ -208,16 +230,10 @@ object OfferManager {
208230
209231 private def waitForInvoice (): Behavior [Command ] = {
210232 Behaviors .receiveMessagePartial {
211- case WrappedInvoiceResponse (invoiceResponse) =>
212- invoiceResponse match {
213- case CreateInvoiceActor .InvoiceCreated (invoice) =>
214- context.log.debug(" invoice created for offerId={} invoice={}" , invoice.invoiceRequest.offer.offerId, invoice.toString)
215- postman ! Postman .SendMessage (OfferTypes .BlindedPath (pathToSender), OnionMessages .RoutingStrategy .FindRoute , TlvStream (OnionMessagePayloadTlv .Invoice (invoice.records)), expectsReply = false , context.messageAdapter[Postman .OnionMessageResponse ](WrappedOnionMessageResponse ))
216- waitForSent()
217- case f : CreateInvoiceActor .InvoiceCreationFailed =>
218- context.log.debug(" invoice creation failed: {}" , f.message)
219- Behaviors .stopped
220- }
233+ case WrappedInvoiceResponse (invoice) =>
234+ context.log.debug(" invoice created for offerId={} invoice={}" , invoice.invoiceRequest.offer.offerId, invoice.toString)
235+ postman ! Postman .SendMessage (OfferTypes .BlindedPath (pathToSender), OnionMessages .RoutingStrategy .FindRoute , TlvStream (OnionMessagePayloadTlv .Invoice (invoice.records)), expectsReply = false , context.messageAdapter[Postman .OnionMessageResponse ](WrappedOnionMessageResponse ))
236+ waitForSent()
221237 }
222238 }
223239
@@ -234,7 +250,8 @@ object OfferManager {
234250 sealed trait Command
235251
236252 /**
237- * Sent by the offer handler. Causes the creation of a dummy invoice that matches as best as possible the actual invoice for this payment (since the actual invoice is not stored) and will be used in the payment handler.
253+ * Sent by the offer handler. Causes the creation of a dummy invoice that matches as best as possible the actual
254+ * invoice for this payment (since the actual invoice is not stored) and will be used in the payment handler.
238255 *
239256 * @param additionalTlvs additional TLVs to add to the dummy invoice. Should be the same as what was used for the actual invoice.
240257 * @param customTlvs custom TLVs to add to the dummy invoice. Should be the same as what was used for the actual invoice.
@@ -246,14 +263,21 @@ object OfferManager {
246263 */
247264 case class RejectPayment (reason : String ) extends Command
248265
249- def apply (nodeParams : NodeParams , replyTo : ActorRef [MultiPartHandler .GetIncomingPaymentActor .Command ], offer : Offer , metadata : MinimalInvoiceData , timeout : FiniteDuration ): Behavior [Command ] = {
266+ def apply (nodeParams : NodeParams ,
267+ replyTo : ActorRef [MultiPartHandler .GetIncomingPaymentActor .Command ],
268+ offer : Offer ,
269+ metadata : MinimalInvoiceData ,
270+ amount : MilliSatoshi ,
271+ timeout : FiniteDuration ): Behavior [Command ] = {
250272 Behaviors .setup { context =>
251273 context.scheduleOnce(timeout, context.self, RejectPayment (" plugin timeout" ))
252274 Behaviors .receiveMessage {
253275 case AcceptPayment (additionalTlvs, customTlvs) =>
254276 val minimalInvoice = MinimalBolt12Invoice (offer, nodeParams.chainHash, metadata.amount, metadata.quantity, Crypto .sha256(metadata.preimage), metadata.payerKey, metadata.createdAt, additionalTlvs, customTlvs)
255277 val incomingPayment = IncomingBlindedPayment (minimalInvoice, metadata.preimage, PaymentType .Blinded , TimestampMilli .now(), IncomingPaymentStatus .Pending )
256- replyTo ! MultiPartHandler .GetIncomingPaymentActor .ProcessPayment (incomingPayment)
278+ // We may be deducing some of the blinded path fees from the received amount.
279+ val maxRecipientPathFees = nodeFee(metadata.recipientPathFees, amount)
280+ replyTo ! MultiPartHandler .GetIncomingPaymentActor .ProcessPayment (incomingPayment, maxRecipientPathFees)
257281 Behaviors .stopped
258282 case RejectPayment (reason) =>
259283 replyTo ! MultiPartHandler .GetIncomingPaymentActor .RejectPayment (reason)
0 commit comments