From cb0f5c5a525c7903134424ece7e1c14511f6cec8 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Mon, 11 Mar 2024 18:31:27 +0100 Subject: [PATCH] Minor API improvements for sending/receiving payments (#619) Define a `NodeEvent.PaymentReceived` that aims to replace the eponym class in the `Peer` (which is marked as deprecated, but otherwise left untouched). The previous impl wasn't emitted for pay-to-open parts. Also define a "synchronous' `sendLightning` method similar to other peer methods. --- .../kotlin/fr/acinq/lightning/NodeEvents.kt | 10 ++++++++ .../kotlin/fr/acinq/lightning/io/Peer.kt | 24 +++++++++++++++++-- .../payment/IncomingPaymentHandler.kt | 12 +++++----- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt b/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt index 7b5630152..76991cda7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt @@ -7,6 +7,9 @@ import fr.acinq.lightning.channel.SharedFundingInput import fr.acinq.lightning.channel.states.ChannelStateWithCommitments import fr.acinq.lightning.channel.states.Normal import fr.acinq.lightning.channel.states.WaitForFundingCreated +import fr.acinq.lightning.db.IncomingPayment +import fr.acinq.lightning.utils.sum +import fr.acinq.lightning.wire.Node import fr.acinq.lightning.wire.PleaseOpenChannel import kotlinx.coroutines.CompletableDeferred @@ -58,3 +61,10 @@ sealed interface SensitiveTaskEvents : NodeEvents { /** This will be emitted in a corner case where the user restores a wallet on an older version of the app, which is unable to read the channel data. */ data object UpgradeRequired : NodeEvents + +sealed interface PaymentEvents : NodeEvents { + data class PaymentReceived(val paymentHash: ByteVector32, val receivedWith: List) : PaymentEvents { + val amount: MilliSatoshi = receivedWith.map { it.amount }.sum() + val fees: MilliSatoshi = receivedWith.map { it.fees }.sum() + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index b433098ab..afb150d13 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -18,6 +18,7 @@ import fr.acinq.lightning.serialization.Encryption.from import fr.acinq.lightning.serialization.Serialization.DeserializationResult import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.* +import fr.acinq.lightning.utils.UUID.Companion.randomUUID import fr.acinq.lightning.wire.* import fr.acinq.lightning.wire.Ping import kotlinx.coroutines.* @@ -77,10 +78,14 @@ data class SendPayment(val paymentId: UUID, val amount: MilliSatoshi, val recipi data class PurgeExpiredPayments(val fromCreatedAt: Long, val toCreatedAt: Long) : PaymentCommand() sealed class PeerEvent +@Deprecated("Replaced by NodeEvents", replaceWith = ReplaceWith("PaymentEvents.PaymentReceived", "fr.acinq.lightning.PaymentEvents")) data class PaymentReceived(val incomingPayment: IncomingPayment, val received: IncomingPayment.Received) : PeerEvent() data class PaymentProgress(val request: SendPayment, val fees: MilliSatoshi) : PeerEvent() -data class PaymentNotSent(val request: SendPayment, val reason: OutgoingPaymentFailure) : PeerEvent() -data class PaymentSent(val request: SendPayment, val payment: LightningOutgoingPayment) : PeerEvent() +sealed class SendPaymentResult : PeerEvent() { + abstract val request: SendPayment +} +data class PaymentNotSent(override val request: SendPayment, val reason: OutgoingPaymentFailure) : SendPaymentResult() +data class PaymentSent(override val request: SendPayment, val payment: LightningOutgoingPayment) : SendPaymentResult() data class ChannelClosing(val channelId: ByteVector32) : PeerEvent() /** @@ -603,6 +608,20 @@ class Peer( } } + suspend fun sendLightning(amount: MilliSatoshi, paymentRequest: Bolt11Invoice): SendPaymentResult { + val res = CompletableDeferred() + val paymentId = randomUUID() + this.launch { + res.complete(eventsFlow + .filterIsInstance() + .filter { it.request.paymentId == paymentId } + .first() + ) + } + send(SendPayment(paymentId, amount, paymentRequest.nodeId, paymentRequest)) + return res.await() + } + suspend fun createInvoice(paymentPreimage: ByteVector32, amount: MilliSatoshi?, description: Either, expirySeconds: Long? = null): Bolt11Invoice { // we add one extra hop which uses a virtual channel with a "peer id", using the highest remote fees and expiry across all // channels to maximize the likelihood of success on the first payment attempt @@ -795,6 +814,7 @@ class Peer( // this was a multi-part payment, we signal that the task is finished nodeParams._nodeEvents.tryEmit(SensitiveTaskEvents.TaskEnded(SensitiveTaskEvents.TaskIdentifier.IncomingMultiPartPayment(result.incomingPayment.paymentHash))) } + @Suppress("DEPRECATION") _eventsFlow.emit(PaymentReceived(result.incomingPayment, result.received)) } is IncomingPaymentHandler.ProcessAddResult.Pending -> if (result.pendingPayment.parts.size == 1) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt index 5c8759657..089ac5162 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt @@ -5,11 +5,8 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto import fr.acinq.bitcoin.PrivateKey import fr.acinq.bitcoin.utils.Either -import fr.acinq.lightning.CltvExpiry +import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes32 -import fr.acinq.lightning.LiquidityEvents -import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.NodeParams import fr.acinq.lightning.channel.ChannelAction import fr.acinq.lightning.channel.ChannelCommand import fr.acinq.lightning.channel.Origin @@ -136,12 +133,14 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPayment ) } when (val origin = action.origin) { - is Origin.PayToOpenOrigin -> + is Origin.PayToOpenOrigin -> { // there already is a corresponding Lightning invoice in the db db.receivePayment( paymentHash = origin.paymentHash, receivedWith = listOf(receivedWith) ) + nodeParams._nodeEvents.emit(PaymentEvents.PaymentReceived(origin.paymentHash, listOf(receivedWith))) + } else -> { // this is a swap, there was no pre-existing invoice, we need to create a fake one val incomingPayment = db.addIncomingPayment( @@ -152,6 +151,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPayment paymentHash = incomingPayment.paymentHash, receivedWith = listOf(receivedWith) ) + nodeParams._nodeEvents.emit(PaymentEvents.PaymentReceived(incomingPayment.paymentHash, listOf(receivedWith))) } } } @@ -299,7 +299,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPayment pending.remove(paymentPart.paymentHash) val received = IncomingPayment.Received(receivedWith = receivedWith) db.receivePayment(paymentPart.paymentHash, received.receivedWith) - + nodeParams._nodeEvents.emit(PaymentEvents.PaymentReceived(paymentPart.paymentHash, received.receivedWith)) return ProcessAddResult.Accepted(actions, incomingPayment.copy(received = received), received) } }