Skip to content

Commit

Permalink
Create invoice outside of the peer's message loop (#492)
Browse files Browse the repository at this point in the history
Invoices creation is independent from the peer, and it would remove the artificial dependency between creating an invoice and connecting to electrum.
  • Loading branch information
pm47 committed Jul 4, 2023
1 parent 5f6d0df commit 3af8927
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 52 deletions.
59 changes: 22 additions & 37 deletions src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ data class WrappedChannelCommand(val channelId: ByteVector32, val channelCommand
object Disconnected : PeerCommand()

sealed class PaymentCommand : PeerCommand()
data class ReceivePayment(val paymentPreimage: ByteVector32, val amount: MilliSatoshi?, val description: Either<String, ByteVector32>, val expirySeconds: Long? = null, val result: CompletableDeferred<PaymentRequest>) : PaymentCommand()
private object CheckPaymentsTimeout : PaymentCommand()
data class PayToOpenResponseCommand(val payToOpenResponse: PayToOpenResponse) : PeerCommand()
data class SendPayment(val paymentId: UUID, val amount: MilliSatoshi, val recipient: PublicKey, val paymentRequest: PaymentRequest, val trampolineFeesOverride: List<TrampolineFees>? = null) : PaymentCommand() {
Expand All @@ -65,7 +64,6 @@ data class SendPayment(val paymentId: UUID, val amount: MilliSatoshi, val recipi
data class PurgeExpiredPayments(val fromCreatedAt: Long, val toCreatedAt: Long) : PaymentCommand()

sealed class PeerEvent
data class PaymentRequestGenerated(val receivePayment: ReceivePayment, val request: String) : PeerEvent()
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()
Expand Down Expand Up @@ -478,16 +476,29 @@ class Peer(
}
}

suspend fun createInvoice(paymentPreimage: ByteVector32, amount: MilliSatoshi?, description: Either<String, ByteVector32>, expirySeconds: Long? = null) {
val command = ReceivePayment(
paymentPreimage = paymentPreimage,
amount = amount,
description = description,
expirySeconds = expirySeconds,
result = CompletableDeferred()
suspend fun createInvoice(paymentPreimage: ByteVector32, amount: MilliSatoshi?, description: Either<String, ByteVector32>, expirySeconds: Long? = null): PaymentRequest {
// 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
val remoteChannelUpdates = _channels.values.mapNotNull { channelState ->
when (channelState) {
is Normal -> channelState.remoteChannelUpdate
is Offline -> (channelState.state as? Normal)?.remoteChannelUpdate
is Syncing -> (channelState.state as? Normal)?.remoteChannelUpdate
else -> null
}
}
val extraHops = listOf(
listOf(
PaymentRequest.TaggedField.ExtraHop(
nodeId = walletParams.trampolineNode.id,
shortChannelId = ShortChannelId.peerId(nodeParams.nodeId),
feeBase = remoteChannelUpdates.maxOfOrNull { it.feeBaseMsat } ?: walletParams.invoiceDefaultRoutingFees.feeBase,
feeProportionalMillionths = remoteChannelUpdates.maxOfOrNull { it.feeProportionalMillionths } ?: walletParams.invoiceDefaultRoutingFees.feeProportional,
cltvExpiryDelta = remoteChannelUpdates.maxOfOrNull { it.cltvExpiryDelta } ?: walletParams.invoiceDefaultRoutingFees.cltvExpiryDelta
)
)
)
send(command)
command.result.await()
return incomingPaymentHandler.createInvoice(paymentPreimage, amount, description, extraHops, expirySeconds)
}

@OptIn(DelicateCoroutinesApi::class)
Expand Down Expand Up @@ -1003,32 +1014,6 @@ class Peer(
_channels = _channels + (msg.temporaryChannelId to state1)
processActions(msg.temporaryChannelId, actions1)
}
is ReceivePayment -> {
// 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
val remoteChannelUpdates = _channels.values.mapNotNull { channelState ->
when (channelState) {
is Normal -> channelState.remoteChannelUpdate
is Offline -> (channelState.state as? Normal)?.remoteChannelUpdate
is Syncing -> (channelState.state as? Normal)?.remoteChannelUpdate
else -> null
}
}
val extraHops = listOf(
listOf(
PaymentRequest.TaggedField.ExtraHop(
nodeId = walletParams.trampolineNode.id,
shortChannelId = ShortChannelId.peerId(nodeParams.nodeId),
feeBase = remoteChannelUpdates.maxOfOrNull { it.feeBaseMsat } ?: walletParams.invoiceDefaultRoutingFees.feeBase,
feeProportionalMillionths = remoteChannelUpdates.maxOfOrNull { it.feeProportionalMillionths } ?: walletParams.invoiceDefaultRoutingFees.feeProportional,
cltvExpiryDelta = remoteChannelUpdates.maxOfOrNull { it.cltvExpiryDelta } ?: walletParams.invoiceDefaultRoutingFees.cltvExpiryDelta
)
)
)
val pr = incomingPaymentHandler.createInvoice(cmd.paymentPreimage, cmd.amount, cmd.description, extraHops, cmd.expirySeconds)
_eventsFlow.emit(PaymentRequestGenerated(cmd, pr.write()))
cmd.result.complete(pr)
}
is PayToOpenResponseCommand -> {
logger.info { "sending ${cmd.payToOpenResponse::class.simpleName}" }
sendToPeer(cmd.payToOpenResponse)
Expand Down
25 changes: 10 additions & 15 deletions src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import fr.acinq.lightning.utils.*
import fr.acinq.lightning.wire.*
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlin.test.*

class PeerTest : LightningTestSuite() {
Expand Down Expand Up @@ -459,9 +460,7 @@ class PeerTest : LightningTestSuite() {
val bob = newPeer(nodeParams, walletParams)

run {
val deferredInvoice = CompletableDeferred<PaymentRequest>()
bob.send(ReceivePayment(randomBytes32(), 1.msat, Either.Left("A description: \uD83D\uDE2C"), 3600L * 3, deferredInvoice))
val invoice = deferredInvoice.await()
val invoice = bob.createInvoice(randomBytes32(), 1.msat, Either.Left("A description: \uD83D\uDE2C"), 3600L * 3)
assertEquals(1.msat, invoice.amount)
assertEquals(3600L * 3, invoice.expirySeconds)
assertEquals("A description: \uD83D\uDE2C", invoice.description)
Expand All @@ -477,9 +476,7 @@ class PeerTest : LightningTestSuite() {
val (_, bob, _, _) = newPeers(this, nodeParams, walletParams, listOf(alice0 to bob0), automateMessaging = false)

run {
val deferredInvoice = CompletableDeferred<PaymentRequest>()
bob.send(ReceivePayment(randomBytes32(), 15_000_000.msat, Either.Left("default routing hints"), null, deferredInvoice))
val invoice = deferredInvoice.await()
val invoice = bob.createInvoice(randomBytes32(), 15_000_000.msat, Either.Left("default routing hints"), null)
// The routing hint uses default values since no channel update has been sent by Alice yet.
assertEquals(1, invoice.routingInfo.size)
assertEquals(1, invoice.routingInfo[0].hints.size)
Expand All @@ -490,10 +487,12 @@ class PeerTest : LightningTestSuite() {
val aliceUpdate =
Announcements.makeChannelUpdate(alice0.staticParams.nodeParams.chainHash, alice0.ctx.privateKey, alice0.staticParams.remoteNodeId, alice0.state.shortChannelId, CltvExpiryDelta(48), 100.msat, 50.msat, 250, 150_000.msat)
bob.forward(aliceUpdate)
// wait until the update is processed
bob.channelsFlow
.map { it.values.first() }
.first { it is Normal && it.remoteChannelUpdate?.feeBaseMsat == 50.msat }

val deferredInvoice = CompletableDeferred<PaymentRequest>()
bob.send(ReceivePayment(randomBytes32(), 5_000_000.msat, Either.Left("updated routing hints"), null, deferredInvoice))
val invoice = deferredInvoice.await()
val invoice = bob.createInvoice(randomBytes32(), 5_000_000.msat, Either.Left("updated routing hints"), null)
// The routing hint uses values from Alice's channel update.
assertEquals(1, invoice.routingInfo.size)
assertEquals(1, invoice.routingInfo[0].hints.size)
Expand All @@ -515,9 +514,7 @@ class PeerTest : LightningTestSuite() {
)
val (alice, bob, alice2bob, bob2alice) = newPeers(this, nodeParams, walletParams, listOf(alice0 to bob0), automateMessaging = false)

val deferredInvoice = CompletableDeferred<PaymentRequest>()
bob.send(ReceivePayment(randomBytes32(), 15_000_000.msat, Either.Left("test invoice"), null, deferredInvoice))
val invoice = deferredInvoice.await()
val invoice = bob.createInvoice(randomBytes32(), 15_000_000.msat, Either.Left("test invoice"), null)

alice.send(SendPayment(UUID.randomUUID(), invoice.amount!!, alice.remoteNodeId, invoice))

Expand Down Expand Up @@ -561,9 +558,7 @@ class PeerTest : LightningTestSuite() {
)
val (alice, bob) = newPeers(this, nodeParams, walletParams, listOf(alice0 to bob0))

val deferredInvoice = CompletableDeferred<PaymentRequest>()
bob.send(ReceivePayment(randomBytes32(), 15_000_000.msat, Either.Left("test invoice"), null, deferredInvoice))
val invoice = deferredInvoice.await()
val invoice = bob.createInvoice(randomBytes32(), 15_000_000.msat, Either.Left("test invoice"), null)

alice.send(SendPayment(UUID.randomUUID(), invoice.amount!!, alice.remoteNodeId, invoice))

Expand Down

0 comments on commit 3af8927

Please sign in to comment.