Skip to content

Commit b18fa6c

Browse files
authored
Rework OutgoingPayment model (#738)
Those are only small simplifications, consistency fixes, and making the interface easier to implement.
1 parent 6aab996 commit b18fa6c

File tree

7 files changed

+211
-200
lines changed

7 files changed

+211
-200
lines changed

modules/core/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt

+79-70
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package fr.acinq.lightning.db
22

33
import fr.acinq.bitcoin.*
4-
import fr.acinq.lightning.Lightning
54
import fr.acinq.lightning.MilliSatoshi
65
import fr.acinq.lightning.ShortChannelId
76
import fr.acinq.lightning.channel.ChannelManagementFees
@@ -33,6 +32,7 @@ interface PaymentsDb : IncomingPaymentsDb, OutgoingPaymentsDb {
3332
}
3433

3534
interface IncomingPaymentsDb {
35+
3636
/** Add a new expected incoming payment (not yet received). */
3737
suspend fun addIncomingPayment(incomingPayment: IncomingPayment)
3838

@@ -62,29 +62,23 @@ interface OutgoingPaymentsDb {
6262
/** Add a new pending outgoing payment (not yet settled). */
6363
suspend fun addOutgoingPayment(outgoingPayment: OutgoingPayment)
6464

65+
/** Add new partial payments to a pending outgoing payment. */
66+
suspend fun addLightningOutgoingPaymentParts(parentId: UUID, parts: List<LightningOutgoingPayment.Part>)
67+
6568
/** Get information about an outgoing payment (settled or not). */
6669
suspend fun getLightningOutgoingPayment(id: UUID): LightningOutgoingPayment?
6770

68-
/** Get information about a liquidity purchase (for which the funding transaction has been signed). */
69-
suspend fun getInboundLiquidityPurchase(fundingTxId: TxId): InboundLiquidityOutgoingPayment?
70-
71-
/** Mark an outgoing payment as completed over Lightning. */
72-
suspend fun completeOutgoingPaymentOffchain(id: UUID, preimage: ByteVector32, completedAt: Long = currentTimestampMillis())
73-
74-
/** Mark an outgoing payment as failed. */
75-
suspend fun completeOutgoingPaymentOffchain(id: UUID, finalFailure: FinalFailure, completedAt: Long = currentTimestampMillis())
76-
77-
/** Add new partial payments to a pending outgoing payment. */
78-
suspend fun addOutgoingLightningParts(parentId: UUID, parts: List<LightningOutgoingPayment.Part>)
71+
/** Get information about an outgoing payment from the id of one of its parts. */
72+
suspend fun getLightningOutgoingPaymentFromPartId(partId: UUID): LightningOutgoingPayment?
7973

80-
/** Mark an outgoing payment part as failed. */
81-
suspend fun completeOutgoingLightningPart(partId: UUID, failure: LightningOutgoingPayment.Part.Status.Failure, completedAt: Long = currentTimestampMillis())
74+
/** Mark a lightning outgoing payment as completed. */
75+
suspend fun completeLightningOutgoingPayment(id: UUID, status: LightningOutgoingPayment.Status.Completed)
8276

83-
/** Mark an outgoing payment part as succeeded. This should not update the parent payment, since some parts may still be pending. */
84-
suspend fun completeOutgoingLightningPart(partId: UUID, preimage: ByteVector32, completedAt: Long = currentTimestampMillis())
77+
/** Mark a lightning outgoing payment part as completed. */
78+
suspend fun completeLightningOutgoingPaymentPart(parentId: UUID, partId: UUID, status: LightningOutgoingPayment.Part.Status.Completed)
8579

86-
/** Get information about an outgoing payment from the id of one of its parts. */
87-
suspend fun getLightningOutgoingPaymentFromPartId(partId: UUID): LightningOutgoingPayment?
80+
/** Get information about a liquidity purchase (for which the funding transaction has been signed). */
81+
suspend fun getInboundLiquidityPurchase(fundingTxId: TxId): InboundLiquidityOutgoingPayment?
8882

8983
/** List all the outgoing payment attempts that tried to pay the given payment hash. */
9084
suspend fun listLightningOutgoingPayments(paymentHash: ByteVector32): List<LightningOutgoingPayment>
@@ -329,8 +323,8 @@ data class LightningOutgoingPayment(
329323
/** This is the total fees that have been paid to make the payment work. It includes the LN routing fees, the fee for the swap-out service, the mining fees for closing a channel. */
330324
override val fees: MilliSatoshi = when (status) {
331325
is Status.Pending -> 0.msat
332-
is Status.Completed.Failed -> 0.msat
333-
is Status.Completed.Succeeded.OffChain -> {
326+
is Status.Failed -> 0.msat
327+
is Status.Succeeded -> {
334328
if (details is Details.SwapOut) {
335329
// The swap-out service takes a fee to cover the miner fee. It's the difference between what we paid the service (recipientAmount) and what goes to the address.
336330
// We also include the routing fee, in case the swap-service is NOT the trampoline node.
@@ -376,15 +370,10 @@ data class LightningOutgoingPayment(
376370
data object Pending : Status()
377371
sealed class Completed : Status() {
378372
abstract val completedAt: Long
379-
380-
data class Failed(val reason: FinalFailure, override val completedAt: Long = currentTimestampMillis()) : Completed()
381-
sealed class Succeeded : Completed() {
382-
data class OffChain(
383-
val preimage: ByteVector32,
384-
override val completedAt: Long = currentTimestampMillis()
385-
) : Succeeded()
386-
}
387373
}
374+
375+
data class Succeeded(val preimage: ByteVector32, override val completedAt: Long = currentTimestampMillis()) : Completed()
376+
data class Failed(val reason: FinalFailure, override val completedAt: Long = currentTimestampMillis()) : Completed()
388377
}
389378

390379
/**
@@ -405,44 +394,49 @@ data class LightningOutgoingPayment(
405394
) {
406395
sealed class Status {
407396
data object Pending : Status()
408-
data class Succeeded(val preimage: ByteVector32, val completedAt: Long = currentTimestampMillis()) : Status()
409-
data class Failed(val failure: Failure, val completedAt: Long = currentTimestampMillis()) : Status()
410-
411-
/**
412-
* User-friendly payment part failure reason, whenever possible.
413-
* Applications should define their own localized message for each of these failure cases.
414-
*/
415-
sealed class Failure {
416-
// @formatter:off
417-
/** The payment is too small: try sending a larger amount. */
418-
data object PaymentAmountTooSmall : Failure() { override fun toString(): String = "the payment amount is too small" }
419-
/** The user has sufficient balance, but the payment is too big: try sending a smaller amount. */
420-
data object PaymentAmountTooBig : Failure() { override fun toString(): String = "the payment amount is too large" }
421-
/** The user doesn't have sufficient balance: try sending a smaller amount. */
422-
data object NotEnoughFunds : Failure() { override fun toString(): String = "not enough funds" }
423-
/** The payment must be retried with more fees to reach the recipient. */
424-
data object NotEnoughFees : Failure() { override fun toString(): String = "routing fees are insufficient" }
425-
/** The payment expiry specified by the recipient is too far away in the future. */
426-
data object PaymentExpiryTooBig : Failure() { override fun toString(): String = "the payment expiry is too far in the future" }
427-
/** There are too many pending payments: wait for them to settle and retry. */
428-
data object TooManyPendingPayments : Failure() { override fun toString(): String = "too many pending payments" }
429-
/** Payments are temporarily paused while a channel is splicing: the payment can be retried after the splice. */
430-
data object ChannelIsSplicing : Failure() { override fun toString(): String = "a splicing operation is in progress" }
431-
/** The channel is closing: another channel should be created to send the payment. */
432-
data object ChannelIsClosing : Failure() { override fun toString(): String = "channel closing is in progress" }
433-
/** Remote failure from an intermediate node in the payment route. */
434-
sealed class RouteFailure : Failure()
435-
/** A remote node had a temporary failure: the payment may succeed if retried. */
436-
data object TemporaryRemoteFailure : RouteFailure() { override fun toString(): String = "a node in the route had a temporary failure" }
437-
/** The payment amount could not be relayed to the recipient, most likely because they don't have enough inbound liquidity. */
438-
data object RecipientLiquidityIssue : RouteFailure() { override fun toString(): String = "liquidity issue at the recipient node" }
439-
/** The payment recipient is offline and could not accept the payment. */
440-
data object RecipientIsOffline : RouteFailure() { override fun toString(): String = "recipient node is offline or unreachable" }
441-
/** The payment recipient received the payment but rejected it. */
442-
data object RecipientRejectedPayment : Failure() { override fun toString(): String = "recipient node rejected the payment" }
443-
/** This is an error that cannot be easily interpreted: we don't know what exactly went wrong and cannot correctly inform the user. */
444-
data class Uninterpretable(val message: String) : Failure() { override fun toString(): String = message }
445-
// @formatter:on
397+
sealed class Completed : Status() {
398+
abstract val completedAt: Long
399+
}
400+
401+
data class Succeeded(val preimage: ByteVector32, override val completedAt: Long = currentTimestampMillis()) : Completed()
402+
data class Failed(val failure: Failure, override val completedAt: Long = currentTimestampMillis()) : Completed() {
403+
404+
/**
405+
* User-friendly payment part failure reason, whenever possible.
406+
* Applications should define their own localized message for each of these failure cases.
407+
*/
408+
sealed class Failure {
409+
// @formatter:off
410+
/** The payment is too small: try sending a larger amount. */
411+
data object PaymentAmountTooSmall : Failure() { override fun toString(): String = "the payment amount is too small" }
412+
/** The user has sufficient balance, but the payment is too big: try sending a smaller amount. */
413+
data object PaymentAmountTooBig : Failure() { override fun toString(): String = "the payment amount is too large" }
414+
/** The user doesn't have sufficient balance: try sending a smaller amount. */
415+
data object NotEnoughFunds : Failure() { override fun toString(): String = "not enough funds" }
416+
/** The payment must be retried with more fees to reach the recipient. */
417+
data object NotEnoughFees : Failure() { override fun toString(): String = "routing fees are insufficient" }
418+
/** The payment expiry specified by the recipient is too far away in the future. */
419+
data object PaymentExpiryTooBig : Failure() { override fun toString(): String = "the payment expiry is too far in the future" }
420+
/** There are too many pending payments: wait for them to settle and retry. */
421+
data object TooManyPendingPayments : Failure() { override fun toString(): String = "too many pending payments" }
422+
/** Payments are temporarily paused while a channel is splicing: the payment can be retried after the splice. */
423+
data object ChannelIsSplicing : Failure() { override fun toString(): String = "a splicing operation is in progress" }
424+
/** The channel is closing: another channel should be created to send the payment. */
425+
data object ChannelIsClosing : Failure() { override fun toString(): String = "channel closing is in progress" }
426+
/** Remote failure from an intermediate node in the payment route. */
427+
sealed class RouteFailure : Failure()
428+
/** A remote node had a temporary failure: the payment may succeed if retried. */
429+
data object TemporaryRemoteFailure : RouteFailure() { override fun toString(): String = "a node in the route had a temporary failure" }
430+
/** The payment amount could not be relayed to the recipient, most likely because they don't have enough inbound liquidity. */
431+
data object RecipientLiquidityIssue : RouteFailure() { override fun toString(): String = "liquidity issue at the recipient node" }
432+
/** The payment recipient is offline and could not accept the payment. */
433+
data object RecipientIsOffline : RouteFailure() { override fun toString(): String = "recipient node is offline or unreachable" }
434+
/** The payment recipient received the payment but rejected it. */
435+
data object RecipientRejectedPayment : Failure() { override fun toString(): String = "recipient node rejected the payment" }
436+
/** This is an error that cannot be easily interpreted: we don't know what exactly went wrong and cannot correctly inform the user. */
437+
data class Uninterpretable(val message: String) : Failure() { override fun toString(): String = message }
438+
// @formatter:on
439+
}
446440
}
447441
}
448442
}
@@ -456,6 +450,25 @@ sealed class OnChainOutgoingPayment : OutgoingPayment() {
456450
abstract override val createdAt: Long
457451
abstract val confirmedAt: Long?
458452
abstract val lockedAt: Long?
453+
override val completedAt: Long? get() = lockedAt
454+
455+
/** Helper method to facilitate updating child classes */
456+
fun setLocked(lockedAt: Long): OnChainOutgoingPayment =
457+
when (this) {
458+
is SpliceOutgoingPayment -> copy(lockedAt = lockedAt)
459+
is SpliceCpfpOutgoingPayment -> copy(lockedAt = lockedAt)
460+
is InboundLiquidityOutgoingPayment -> copy(lockedAt = lockedAt)
461+
is ChannelCloseOutgoingPayment -> copy(lockedAt = lockedAt)
462+
}
463+
464+
/** Helper method to facilitate updating child classes */
465+
fun setConfirmed(confirmedAt: Long): OnChainOutgoingPayment =
466+
when (this) {
467+
is SpliceOutgoingPayment -> copy(confirmedAt = confirmedAt)
468+
is SpliceCpfpOutgoingPayment -> copy(confirmedAt = confirmedAt)
469+
is InboundLiquidityOutgoingPayment -> copy(confirmedAt = confirmedAt)
470+
is ChannelCloseOutgoingPayment -> copy(confirmedAt = confirmedAt)
471+
}
459472
}
460473

461474
data class SpliceOutgoingPayment(
@@ -471,7 +484,6 @@ data class SpliceOutgoingPayment(
471484
) : OnChainOutgoingPayment() {
472485
override val amount: MilliSatoshi = (recipientAmount + miningFees).toMilliSatoshi()
473486
override val fees: MilliSatoshi = miningFees.toMilliSatoshi()
474-
override val completedAt: Long? = confirmedAt
475487
}
476488

477489
data class SpliceCpfpOutgoingPayment(
@@ -485,7 +497,6 @@ data class SpliceCpfpOutgoingPayment(
485497
) : OnChainOutgoingPayment() {
486498
override val amount: MilliSatoshi = miningFees.toMilliSatoshi()
487499
override val fees: MilliSatoshi = miningFees.toMilliSatoshi()
488-
override val completedAt: Long? = confirmedAt
489500
}
490501

491502
data class InboundLiquidityOutgoingPayment(
@@ -502,7 +513,6 @@ data class InboundLiquidityOutgoingPayment(
502513
val serviceFees: Satoshi = purchase.fees.serviceFee
503514
override val fees: MilliSatoshi = (localMiningFees + purchase.fees.total).toMilliSatoshi()
504515
override val amount: MilliSatoshi = fees
505-
override val completedAt: Long? = lockedAt
506516
val fundingFee: LiquidityAds.FundingFee = LiquidityAds.FundingFee(purchase.fees.total.toMilliSatoshi(), txId)
507517
/**
508518
* Even in the "from future htlc" case the mining fee corresponding to the previous channel output
@@ -555,7 +565,6 @@ data class ChannelCloseOutgoingPayment(
555565
) : OnChainOutgoingPayment() {
556566
override val amount: MilliSatoshi = (recipientAmount + miningFees).toMilliSatoshi()
557567
override val fees: MilliSatoshi = miningFees.toMilliSatoshi()
558-
override val completedAt: Long? = confirmedAt
559568
}
560569

561570
data class HopDesc(val nodeId: PublicKey, val nextNodeId: PublicKey, val shortChannelId: ShortChannelId? = null) {

0 commit comments

Comments
 (0)