diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt index 6c5b7e045..dd86444fe 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt @@ -12,6 +12,7 @@ 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.db.LightningIncomingPayment import fr.acinq.lightning.db.OutgoingPayment import fr.acinq.lightning.utils.sum import fr.acinq.lightning.wire.Init @@ -70,9 +71,6 @@ sealed interface SensitiveTaskEvents : NodeEvents { data object UpgradeRequired : NodeEvents sealed interface PaymentEvents : NodeEvents { - data class PaymentReceived(val paymentHash: ByteVector32, val receivedWith: List) : PaymentEvents { - val amount: MilliSatoshi = receivedWith.map { it.amountReceived }.sum() - val fees: MilliSatoshi = receivedWith.map { it.fees }.sum() - } + data class PaymentReceived(val payment: IncomingPayment) : PaymentEvents data class PaymentSent(val payment: OutgoingPayment) : PaymentEvents } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt index 56d3a7ee9..2a4494e8d 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt @@ -9,7 +9,6 @@ import fr.acinq.lightning.channel.states.PersistedChannelState import fr.acinq.lightning.db.ChannelClosingType import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.UUID -import fr.acinq.lightning.utils.toMilliSatoshi import fr.acinq.lightning.wire.* /** Channel Actions (outputs produced by the state machine). */ @@ -80,7 +79,7 @@ sealed class ChannelAction { abstract val txId: TxId abstract val localInputs: Set data class ViaNewChannel(val amountReceived: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment() - data class ViaSpliceIn(val amountReceived: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment() + data class ViaSpliceIn(val amountReceived: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment() } /** Payment sent through on-chain operations (channel close or splice-out) */ sealed class StoreOutgoingPayment : Storage() { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index ad0e24621..aa9074126 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -883,7 +883,6 @@ data class Normal( if (action.fundingTx.sharedTx.tx.localInputs.isNotEmpty()) add( ChannelAction.Storage.StoreIncomingPayment.ViaSpliceIn( amountReceived = action.fundingTx.sharedTx.tx.localInputs.map { i -> i.txOut.amount }.sum().toMilliSatoshi() - action.fundingTx.sharedTx.tx.localFees, - serviceFee = 0.msat, miningFee = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), localInputs = action.fundingTx.sharedTx.tx.localInputs.map { it.outPoint }.toSet(), txId = action.fundingTx.txId, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt index 09bc12e4e..5b28f247a 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.db import fr.acinq.bitcoin.* +import fr.acinq.lightning.Lightning import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.channel.ChannelManagementFees @@ -33,28 +34,28 @@ interface PaymentsDb : IncomingPaymentsDb, OutgoingPaymentsDb { interface IncomingPaymentsDb { /** Add a new expected incoming payment (not yet received). */ - suspend fun addIncomingPayment(preimage: ByteVector32, origin: IncomingPayment.Origin, createdAt: Long = currentTimestampMillis()): IncomingPayment + suspend fun addIncomingPayment(incomingPayment: IncomingPayment) /** Get information about an incoming payment (paid or not) for the given payment hash, if any. */ - suspend fun getIncomingPayment(paymentHash: ByteVector32): IncomingPayment? + suspend fun getLightningIncomingPayment(paymentHash: ByteVector32): LightningIncomingPayment? /** * Mark an incoming payment as received (paid). * Note that this function assumes that there is a matching payment request in the DB, otherwise it will be a no-op. * * This method is additive: - * - receivedWith set is appended to the existing set in database. + * - parts list is appended to the existing list in database. * - receivedAt must be updated in database. * - * @param receivedWith Is a set containing the payment parts holding the incoming amount. + * @param parts Is a list containing the payment parts holding the incoming amount. */ - suspend fun receivePayment(paymentHash: ByteVector32, receivedWith: List, receivedAt: Long = currentTimestampMillis()) + suspend fun receiveLightningPayment(paymentHash: ByteVector32, parts: List) /** List expired unpaid normal payments created within specified time range (with the most recent payments first). */ - suspend fun listExpiredPayments(fromCreatedAt: Long = 0, toCreatedAt: Long = currentTimestampMillis()): List + suspend fun listLightningExpiredPayments(fromCreatedAt: Long = 0, toCreatedAt: Long = currentTimestampMillis()): List /** Remove a pending incoming payment.*/ - suspend fun removeIncomingPayment(paymentHash: ByteVector32): Boolean + suspend fun removeLightningIncomingPayment(paymentHash: ByteVector32): Boolean } interface OutgoingPaymentsDb { @@ -91,6 +92,8 @@ interface OutgoingPaymentsDb { /** A payment made to or from the wallet. */ sealed class WalletPayment { + abstract val id: UUID + /** Absolute time in milliseconds since UNIX epoch when the payment was created. */ abstract val createdAt: Long @@ -108,70 +111,47 @@ sealed class WalletPayment { abstract val amount: MilliSatoshi } +sealed class IncomingPayment : WalletPayment() { + /** Amount received for this part after applying the fees. This is the final amount we can use. */ + abstract val amountReceived: MilliSatoshi + override val amount: MilliSatoshi get() = amountReceived +} + /** * An incoming payment received by this node. * At first it is in a pending state, then will become either a success (if we receive a matching payment) or a failure (if the payment request expires). * - * @param preimage payment preimage, which acts as a proof-of-payment for the payer. - * @param origin origin of a payment (normal, swap, etc). - * @param received funds received for this payment, null if no funds have been received yet. - * @param createdAt absolute time in milliseconds since UNIX epoch when the payment request was generated. + * @param paymentPreimage payment preimage, which acts as a proof-of-payment for the payer. */ -data class IncomingPayment(val preimage: ByteVector32, val origin: Origin, val received: Received?, override val createdAt: Long = currentTimestampMillis()) : WalletPayment() { +sealed class LightningIncomingPayment(val paymentPreimage: ByteVector32) : IncomingPayment() { - val paymentHash: ByteVector32 = Crypto.sha256(preimage).toByteVector32() + val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage).toByteVector32() - /** - * This timestamp will be defined when the payment is final and usable for spending: - * - for lightning payment it is instant. - * - for on-chain payments, the associated transaction doesn't necessarily need to be - * confirmed (if zero-conf is used), but both sides have to agree that the funds are - * usable, a.k.a. "locked". - */ - override val completedAt: Long? - get() = when { - received == null -> null // payment has not yet been received - received.receivedWith.any { it is ReceivedWith.OnChainIncomingPayment && it.lockedAt == null } -> null // payment has been received, but there is at least one unconfirmed on-chain part - else -> received.receivedAt - } + override val id: UUID = UUID.fromBytes(paymentHash.toByteArray().copyOf(16)) - /** Total fees paid to receive this payment. */ - override val fees: MilliSatoshi = received?.fees ?: 0.msat + /** Funds received for this payment, empty if no funds have been received yet. */ + abstract val parts: List - /** Total amount actually received for this payment after applying the fees. If someone sent you 500 and the fee was 10, this amount will be 490. */ - override val amount: MilliSatoshi = received?.amount ?: 0.msat + /** This timestamp will be defined when the received amount is usable for spending. */ + override val completedAt: Long? get() = parts.maxByOrNull { it.receivedAt }?.receivedAt - sealed class Origin { - /** A normal, Bolt11 invoice-based lightning payment. */ - data class Invoice(val paymentRequest: Bolt11Invoice) : Origin() - - /** A payment for a Bolt 12 offer: note that we only keep a few fields from the corresponding Bolt 12 invoice. */ - data class Offer(val metadata: OfferPaymentMetadata) : Origin() - - /** DEPRECATED: this is the legacy trusted swap-in, which we keep for backwards-compatibility (previous payments inside the DB). */ - data class SwapIn(val address: String?) : Origin() - - /** Trustless swap-in (dual-funding or splice-in) */ - data class OnChain(val txId: TxId, val localInputs: Set) : Origin() - } - - data class Received(val receivedWith: List, val receivedAt: Long = currentTimestampMillis()) { - /** Total amount received after applying the fees. */ - val amount: MilliSatoshi = receivedWith.map { it.amountReceived }.sum() + /** Total fees paid to receive this payment. */ + override val fees: MilliSatoshi get() = parts.map { it.fees }.sum() - /** Fees applied to receive this payment. */ - val fees: MilliSatoshi = receivedWith.map { it.fees }.sum() - } + /** Total amount actually received for this payment after applying the fees. If someone sent you 500 and the fee was 10, this amount will be 490. */ + override val amountReceived: MilliSatoshi get() = parts.map { it.amountReceived }.sum() - sealed class ReceivedWith { + sealed class Part { /** Amount received for this part after applying the fees. This is the final amount we can use. */ abstract val amountReceived: MilliSatoshi - /** Fees applied to receive this part. Is zero for Lightning payments. */ + /** Fees applied to receive this part.*/ abstract val fees: MilliSatoshi + abstract val receivedAt: Long + /** Payment was received via existing lightning channels. */ - data class LightningPayment(override val amountReceived: MilliSatoshi, val channelId: ByteVector32, val htlcId: Long, val fundingFee: LiquidityAds.FundingFee?) : ReceivedWith() { + data class Htlc(override val amountReceived: MilliSatoshi, val channelId: ByteVector32, val htlcId: Long, val fundingFee: LiquidityAds.FundingFee?, override val receivedAt: Long = currentTimestampMillis()) : Part() { // If there is no funding fee, the fees are paid by the sender for lightning payments. override val fees: MilliSatoshi = fundingFee?.amount ?: 0.msat } @@ -180,58 +160,143 @@ data class IncomingPayment(val preimage: ByteVector32, val origin: Origin, val r * Payment was added to our fee credit for future on-chain operations (see [fr.acinq.lightning.Feature.FundingFeeCredit]). * We didn't really receive this amount yet, but we trust our peer to use it for future on-chain operations. */ - data class AddedToFeeCredit(override val amountReceived: MilliSatoshi) : ReceivedWith() { + data class FeeCredit(override val amountReceived: MilliSatoshi, override val receivedAt: Long = currentTimestampMillis()) : Part() { // Adding to the fee credit doesn't cost any fees. override val fees: MilliSatoshi = 0.msat } + } - sealed class OnChainIncomingPayment : ReceivedWith() { - abstract val serviceFee: MilliSatoshi - abstract val miningFee: Satoshi - override val fees: MilliSatoshi get() = serviceFee + miningFee.toMilliSatoshi() - abstract val channelId: ByteVector32 - abstract val txId: TxId - abstract val confirmedAt: Long? - abstract val lockedAt: Long? - } + /** A payment expires if it is a [Bolt11IncomingPayment] and its invoice has expired. */ + fun isExpired(): Boolean = this is Bolt11IncomingPayment && this.paymentRequest.isExpired() - /** - * Payment was received via a new channel opened to us. - * - * @param amountReceived Our side of the balance of this channel when it's created. This is the amount received after the creation fees are applied. - * @param serviceFee Fees paid to Lightning Service Provider to open this channel. - * @param miningFee Feed paid to bitcoin miners for processing the L1 transaction. - * @param channelId The long id of the channel created to receive this payment. May be null if the channel id is not known. - */ - data class NewChannel( - override val amountReceived: MilliSatoshi, - override val serviceFee: MilliSatoshi, - override val miningFee: Satoshi, - override val channelId: ByteVector32, - override val txId: TxId, - override val confirmedAt: Long?, - override val lockedAt: Long? - ) : OnChainIncomingPayment() - - data class SpliceIn( - override val amountReceived: MilliSatoshi, - override val serviceFee: MilliSatoshi, - override val miningFee: Satoshi, - override val channelId: ByteVector32, - override val txId: TxId, - override val confirmedAt: Long?, - override val lockedAt: Long? - ) : OnChainIncomingPayment() + /** Helper method to facilitate updating child classes */ + fun addReceivedParts(parts: List): LightningIncomingPayment { + return when (this) { + is Bolt11IncomingPayment -> copy(parts = this.parts + parts) + is Bolt12IncomingPayment -> copy(parts = this.parts + parts) + } } +} - /** A payment expires if its origin is [Origin.Invoice] and its invoice has expired. */ - fun isExpired(): Boolean = origin is Origin.Invoice && origin.paymentRequest.isExpired() +/** A normal, Bolt11 invoice-based lightning payment. */ +data class Bolt11IncomingPayment( + private val preimage: ByteVector32, + val paymentRequest: Bolt11Invoice, + override val parts: List = emptyList(), + override val createdAt: Long = currentTimestampMillis() +) : LightningIncomingPayment(preimage) + +/** A payment for a Bolt 12 offer: note that we only keep a few fields from the corresponding Bolt 12 invoice. */ +data class Bolt12IncomingPayment( + private val preimage: ByteVector32, + val metadata: OfferPaymentMetadata, + override val parts: List = emptyList(), + override val createdAt: Long = currentTimestampMillis() +) : LightningIncomingPayment(preimage) + +/** Trustless swap-in (dual-funding or splice-in) */ +sealed class OnChainIncomingPayment : IncomingPayment() { + abstract override val id: UUID + /** Fees paid to Lightning Service Provider for this on-chain transaction. */ + abstract val serviceFee: MilliSatoshi + /** Feed paid to bitcoin miners for processing the on-chain operation. */ + abstract val miningFee: Satoshi + override val fees: MilliSatoshi get() = serviceFee + miningFee.toMilliSatoshi() + abstract val channelId: ByteVector32 + abstract val txId: TxId + abstract val localInputs: Set + abstract val confirmedAt: Long? + abstract val lockedAt: Long? + /** + * This timestamp will be defined when the received amount is usable for spending. The + * associated transaction doesn't necessarily need to be confirmed (if zero-conf is + * used), but both sides have to agree that the funds are usable, a.k.a. "locked". + */ + override val completedAt: Long? get() = lockedAt + + /** Helper method to facilitate updating child classes */ + fun setLocked(lockedAt: Long): OnChainIncomingPayment = + when (this) { + is NewChannelIncomingPayment -> copy(lockedAt = lockedAt) + is SpliceInIncomingPayment -> copy(lockedAt = lockedAt) + } + + /** Helper method to facilitate updating child classes */ + fun setConfirmed(confirmedAt: Long): OnChainIncomingPayment = + when (this) { + is NewChannelIncomingPayment -> copy(confirmedAt = confirmedAt) + is SpliceInIncomingPayment -> copy(confirmedAt = confirmedAt) + } } -sealed class OutgoingPayment : WalletPayment() { - abstract val id: UUID +/** + * Payment was received via a new channel opened to us. + * + * @param amountReceived Our side of the balance of this channel when it's created. This is the amount received after the creation fees are applied. + */ +data class NewChannelIncomingPayment( + override val id: UUID, + override val amountReceived: MilliSatoshi, + override val serviceFee: MilliSatoshi, + override val miningFee: Satoshi, + override val channelId: ByteVector32, + override val txId: TxId, + override val localInputs: Set, + override val createdAt: Long, + override val confirmedAt: Long?, + override val lockedAt: Long? +) : OnChainIncomingPayment() + +/** Payment was received by splicing on-chain local funds into an existing channel. */ +data class SpliceInIncomingPayment( + override val id: UUID, + override val amountReceived: MilliSatoshi, + override val miningFee: Satoshi, + override val channelId: ByteVector32, + override val txId: TxId, + override val localInputs: Set, + override val createdAt: Long, + override val confirmedAt: Long?, + override val lockedAt: Long? +) : OnChainIncomingPayment() { + override val serviceFee: MilliSatoshi = 0.msat +} + +@Deprecated("Legacy trusted swap-in, kept for backwards-compatibility with existing databases.") +data class LegacySwapInIncomingPayment( + override val id: UUID, + override val amountReceived: MilliSatoshi, + override val fees: MilliSatoshi, + val address: String?, + override val createdAt: Long, + override val completedAt: Long? +) : IncomingPayment() + +@Deprecated("Legacy pay-to-open/pay-to-splice, kept for backwards-compatibility with existing databases. Those payments can be a mix of lightning parts and on-chain parts, and either Bolt11 or Bolt12.") +data class LegacyPayToOpenIncomingPayment( + val paymentPreimage: ByteVector32, + val origin: Origin, + val parts: List, + override val createdAt: Long, + override val completedAt: Long? +) : IncomingPayment() { + val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage).toByteVector32() + override val id: UUID = UUID.fromBytes(paymentHash.toByteArray().copyOf(16)) + override val amountReceived: MilliSatoshi = parts.map { it.amountReceived }.sum() + override val fees: MilliSatoshi = parts.filterIsInstance().map { it.serviceFee + it.miningFee.toMilliSatoshi() }.sum() + sealed class Origin { + data class Invoice(val paymentRequest: Bolt11Invoice) : Origin() + data class Offer(val metadata: OfferPaymentMetadata) : Origin() + } + sealed class Part { + abstract val amountReceived: MilliSatoshi + data class Lightning(override val amountReceived: MilliSatoshi, val channelId: ByteVector32, val htlcId: Long) : Part() + data class OnChain(override val amountReceived: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, val channelId: ByteVector32, val txId: TxId, val confirmedAt: Long?, val lockedAt: Long?) : Part() + } } +sealed class OutgoingPayment : WalletPayment() + /** * An outgoing payment sent by this node. * The payment may be split in multiple parts, which may fail, be retried, and then either succeed or fail. diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index ba7833d6e..6e4e9e932 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -138,8 +138,6 @@ data class SendOnTheFlyFundingMessage(val message: OnTheFlyFundingMessage) : Pee 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() sealed class SendPaymentResult : PeerEvent() { abstract val request: SendPayment @@ -847,8 +845,36 @@ class Peer( action.htlcs.forEach { db.channels.addHtlcInfo(actualChannelId, it.commitmentNumber, it.paymentHash, it.cltvExpiry) } } is ChannelAction.Storage.StoreIncomingPayment -> { - logger.info { "storing incoming payment $action" } - incomingPaymentHandler.process(actualChannelId, action) + logger.info { "storing $action" } + val payment = when (action) { + is ChannelAction.Storage.StoreIncomingPayment.ViaNewChannel -> + NewChannelIncomingPayment( + id = UUID.randomUUID(), + amountReceived = action.amountReceived, + serviceFee = action.serviceFee, + miningFee = action.miningFee, + channelId = channelId, + txId = action.txId, + localInputs = action.localInputs, + createdAt = currentTimestampMillis(), + confirmedAt = null, + lockedAt = null, + ) + is ChannelAction.Storage.StoreIncomingPayment.ViaSpliceIn -> + SpliceInIncomingPayment( + id = UUID.randomUUID(), + amountReceived = action.amountReceived, + miningFee = action.miningFee, + channelId = channelId, + txId = action.txId, + localInputs = action.localInputs, + createdAt = currentTimestampMillis(), + confirmedAt = null, + lockedAt = null, + ) + } + nodeParams._nodeEvents.emit(PaymentEvents.PaymentReceived(payment)) + db.payments.addIncomingPayment(payment) } is ChannelAction.Storage.StoreOutgoingPayment -> { logger.info { "storing $action" } @@ -938,12 +964,10 @@ class Peer( } when (result) { is IncomingPaymentHandler.ProcessAddResult.Accepted -> { - if ((result.incomingPayment.received?.receivedWith?.size ?: 0) > 1) { + if (result.incomingPayment.parts.size > 1) { // 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) { // this is the first part of a multi-part payment, we request to keep the app alive to receive subsequent parts diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt index a440c2c18..4deadcfd3 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt @@ -7,8 +7,7 @@ import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* import fr.acinq.lightning.crypto.sphinx.Sphinx -import fr.acinq.lightning.db.IncomingPayment -import fr.acinq.lightning.db.PaymentsDb +import fr.acinq.lightning.db.* import fr.acinq.lightning.io.AddLiquidityForIncomingPayment import fr.acinq.lightning.io.PeerCommand import fr.acinq.lightning.io.SendOnTheFlyFundingMessage @@ -48,9 +47,11 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { sealed class ProcessAddResult { abstract val actions: List - data class Accepted(override val actions: List, val incomingPayment: IncomingPayment, val received: IncomingPayment.Received) : ProcessAddResult() - data class Rejected(override val actions: List, val incomingPayment: IncomingPayment?) : ProcessAddResult() - data class Pending(val incomingPayment: IncomingPayment, val pendingPayment: PendingPayment, override val actions: List = listOf()) : ProcessAddResult() + data class Accepted(override val actions: List, val incomingPayment: LightningIncomingPayment, val parts: List) : ProcessAddResult() { + val amount = parts.map { it.amountReceived }.sum() + } + data class Rejected(override val actions: List, val incomingPayment: LightningIncomingPayment?) : ProcessAddResult() + data class Pending(val incomingPayment: LightningIncomingPayment, val pendingPayment: PendingPayment, override val actions: List = listOf()) : ProcessAddResult() } /** @@ -101,55 +102,11 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { timestampSeconds ) logger.info(mapOf("paymentHash" to paymentHash)) { "generated payment request ${pr.write()}" } - db.addIncomingPayment(paymentPreimage, IncomingPayment.Origin.Invoice(pr)) + val incomingPayment = Bolt11IncomingPayment(paymentPreimage, pr) + db.addIncomingPayment(incomingPayment) return pr } - /** Save the "received-with" details of an incoming on-chain amount. */ - suspend fun process(channelId: ByteVector32, action: ChannelAction.Storage.StoreIncomingPayment) { - val receivedWith = when (action) { - is ChannelAction.Storage.StoreIncomingPayment.ViaNewChannel -> - IncomingPayment.ReceivedWith.NewChannel( - amountReceived = action.amountReceived, - serviceFee = action.serviceFee, - miningFee = action.miningFee, - channelId = channelId, - txId = action.txId, - confirmedAt = null, - lockedAt = null, - ) - is ChannelAction.Storage.StoreIncomingPayment.ViaSpliceIn -> - IncomingPayment.ReceivedWith.SpliceIn( - amountReceived = action.amountReceived, - serviceFee = action.serviceFee, - miningFee = action.miningFee, - channelId = channelId, - txId = action.txId, - confirmedAt = null, - lockedAt = null, - ) - } - when (action.origin) { - is Origin.OnChainWallet -> { - // this is a swap, there was no pre-existing invoice, we need to create a fake one - val incomingPayment = db.addIncomingPayment( - preimage = randomBytes32(), // not used, placeholder - origin = IncomingPayment.Origin.OnChain(action.txId, action.localInputs) - ) - db.receivePayment( - paymentHash = incomingPayment.paymentHash, - receivedWith = listOf(receivedWith) - ) - nodeParams._nodeEvents.emit(PaymentEvents.PaymentReceived(incomingPayment.paymentHash, listOf(receivedWith))) - } - is Origin.OffChainPayment -> { - // There is nothing to do, since we haven't been paid anything in the funding/splice transaction. - // We will receive HTLCs later for the payment that triggered the on-the-fly funding transaction. - } - null -> {} - } - } - /** Process an incoming htlc. Before calling this, the htlc must be committed and ack-ed by both peers. */ suspend fun process(htlc: UpdateAddHtlc, remoteFeatures: Features, currentBlockHeight: Int, currentFeerate: FeeratePerKw, remoteFundingRates: LiquidityAds.WillFundRates?, currentFeeCredit: MilliSatoshi = 0.msat): ProcessAddResult { return process(Either.Right(htlc), remoteFeatures, currentBlockHeight, currentFeerate, remoteFundingRates, currentFeeCredit) @@ -194,7 +151,8 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { is Either.Left -> validationResult.value is Either.Right -> { val incomingPayment = validationResult.value - if (incomingPayment.received != null) { + val receivedParts = incomingPayment.parts + if (receivedParts.isNotEmpty()) { return when (paymentPart) { is HtlcPart -> { // The invoice for this payment hash has already been paid. Two possible scenarios: @@ -206,11 +164,11 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { // // 2) This is a new htlc. This can happen when a sender pays an already paid invoice. In that case the // htlc can be safely rejected. - val htlcsMapInDb = incomingPayment.received.receivedWith.filterIsInstance().map { it.channelId to it.htlcId } + val htlcsMapInDb = receivedParts.filterIsInstance().map { it.channelId to it.htlcId } if (htlcsMapInDb.contains(paymentPart.htlc.channelId to paymentPart.htlc.id)) { logger.info { "accepting local replay of htlc=${paymentPart.htlc.id} on channel=${paymentPart.htlc.channelId}" } - val action = WrappedChannelCommand(paymentPart.htlc.channelId, ChannelCommand.Htlc.Settlement.Fulfill(paymentPart.htlc.id, incomingPayment.preimage, true)) - ProcessAddResult.Accepted(listOf(action), incomingPayment, incomingPayment.received) + val action = WrappedChannelCommand(paymentPart.htlc.channelId, ChannelCommand.Htlc.Settlement.Fulfill(paymentPart.htlc.id, incomingPayment.paymentPreimage, true)) + ProcessAddResult.Accepted(listOf(action), incomingPayment, receivedParts) } else { logger.info { "rejecting htlc part for an invoice that has already been paid" } val action = actionForFailureMessage(IncorrectOrUnknownPaymentDetails(paymentPart.totalAmount, currentBlockHeight.toLong()), paymentPart.htlc) @@ -276,16 +234,16 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { when { addToFeeCredit -> { logger.info { "adding on-the-fly funding to fee credit (amount=${willAddHtlcParts.map { it.amount }.sum()})" } - val receivedWith = buildList { - htlcParts.forEach { add(IncomingPayment.ReceivedWith.LightningPayment(it.amount, it.htlc.channelId, it.htlc.id, it.htlc.fundingFee)) } - willAddHtlcParts.forEach { add(IncomingPayment.ReceivedWith.AddedToFeeCredit(it.amount)) } + val parts = buildList { + htlcParts.forEach { add(LightningIncomingPayment.Part.Htlc(it.amount, it.htlc.channelId, it.htlc.id, it.htlc.fundingFee)) } + willAddHtlcParts.forEach { add(LightningIncomingPayment.Part.FeeCredit(it.amount)) } } val actions = buildList { // We send a single add_fee_credit for the will_add_htlc set. - add(SendOnTheFlyFundingMessage(AddFeeCredit(nodeParams.chainHash, incomingPayment.preimage))) - htlcParts.forEach { add(WrappedChannelCommand(it.htlc.channelId, ChannelCommand.Htlc.Settlement.Fulfill(it.htlc.id, incomingPayment.preimage, true))) } + add(SendOnTheFlyFundingMessage(AddFeeCredit(nodeParams.chainHash, incomingPayment.paymentPreimage))) + htlcParts.forEach { add(WrappedChannelCommand(it.htlc.channelId, ChannelCommand.Htlc.Settlement.Fulfill(it.htlc.id, incomingPayment.paymentPreimage, true))) } } - acceptPayment(incomingPayment, receivedWith, actions) + acceptPayment(incomingPayment, parts, actions) } else -> { // We're not adding to our fee credit, so we need to check our liquidity policy. @@ -298,7 +256,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { rejectPayment(payment, incomingPayment, TemporaryNodeFailure) } else -> { - val actions = listOf(AddLiquidityForIncomingPayment(payment.amountReceived, requestedAmount, fundingRate, incomingPayment.preimage, willAddHtlcParts.map { it.htlc })) + val actions = listOf(AddLiquidityForIncomingPayment(payment.amountReceived, requestedAmount, fundingRate, incomingPayment.paymentPreimage, willAddHtlcParts.map { it.htlc })) val paymentOnlyHtlcs = payment.copy( // We need to splice before receiving the remaining HTLC parts. // We extend the duration of the MPP timeout to give more time for funding to complete. @@ -324,12 +282,12 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { rejectPayment(payment, incomingPayment, failure) } is Either.Right -> { - val receivedWith = htlcParts.map { part -> IncomingPayment.ReceivedWith.LightningPayment(part.amount, part.htlc.channelId, part.htlc.id, part.htlc.fundingFee) } + val parts = htlcParts.map { part -> LightningIncomingPayment.Part.Htlc(part.amount, part.htlc.channelId, part.htlc.id, part.htlc.fundingFee) } val actions = htlcParts.map { part -> - val cmd = ChannelCommand.Htlc.Settlement.Fulfill(part.htlc.id, incomingPayment.preimage, true) + val cmd = ChannelCommand.Htlc.Settlement.Fulfill(part.htlc.id, incomingPayment.paymentPreimage, true) WrappedChannelCommand(part.htlc.channelId, cmd) } - acceptPayment(incomingPayment, receivedWith, actions) + acceptPayment(incomingPayment, parts, actions) } } } @@ -340,20 +298,20 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { } } - private suspend fun acceptPayment(incomingPayment: IncomingPayment, receivedWith: List, actions: List): ProcessAddResult.Accepted { + private suspend fun acceptPayment(incomingPayment: LightningIncomingPayment, parts: List, actions: List): ProcessAddResult.Accepted { pending.remove(incomingPayment.paymentHash) - if (incomingPayment.origin is IncomingPayment.Origin.Offer) { + if (incomingPayment is Bolt12IncomingPayment) { // We didn't store the Bolt 12 invoice in our DB when receiving the invoice_request (to protect against DoS). // We need to create the DB entry now otherwise the payment won't be recorded. - db.addIncomingPayment(incomingPayment.preimage, incomingPayment.origin) + db.addIncomingPayment(incomingPayment) } - db.receivePayment(incomingPayment.paymentHash, receivedWith) - nodeParams._nodeEvents.emit(PaymentEvents.PaymentReceived(incomingPayment.paymentHash, receivedWith)) - val received = IncomingPayment.Received(receivedWith) - return ProcessAddResult.Accepted(actions, incomingPayment.copy(received = received), received) + db.receiveLightningPayment(incomingPayment.paymentHash, parts) + val incomingPayment1 = incomingPayment.addReceivedParts(parts) + nodeParams._nodeEvents.emit(PaymentEvents.PaymentReceived(incomingPayment1)) + return ProcessAddResult.Accepted(actions, incomingPayment1, parts) } - private fun rejectPayment(payment: PendingPayment, incomingPayment: IncomingPayment, failure: FailureMessage): ProcessAddResult.Rejected { + private fun rejectPayment(payment: PendingPayment, incomingPayment: LightningIncomingPayment, failure: FailureMessage): ProcessAddResult.Rejected { pending.remove(incomingPayment.paymentHash) val actions = payment.parts.map { part -> when (part) { @@ -438,11 +396,11 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { } } - private suspend fun validatePaymentPart(paymentPart: PaymentPart, currentBlockHeight: Int): Either { + private suspend fun validatePaymentPart(paymentPart: PaymentPart, currentBlockHeight: Int): Either { val logger = MDCLogger(logger.logger, staticMdc = paymentPart.mdc()) when (val finalPayload = paymentPart.finalPayload) { is PaymentOnion.FinalPayload.Standard -> { - val incomingPayment = db.getIncomingPayment(paymentPart.paymentHash) + val incomingPayment = db.getLightningIncomingPayment(paymentPart.paymentHash) return when { incomingPayment == null -> { logger.warning { "payment for which we don't have a preimage" } @@ -450,15 +408,15 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { } // Payments are rejected for expired invoices UNLESS invoice has already been paid // We must accept payments for already paid invoices, because it could be the channel replaying HTLCs that we already fulfilled - incomingPayment.isExpired() && incomingPayment.received == null -> { + incomingPayment.isExpired() && incomingPayment.parts.isEmpty() -> { logger.warning { "the invoice is expired" } Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight)) } - incomingPayment.origin !is IncomingPayment.Origin.Invoice -> { - logger.warning { "unsupported payment type: ${incomingPayment.origin::class}" } + incomingPayment !is Bolt11IncomingPayment -> { + logger.warning { "unsupported payment type: ${incomingPayment::class}" } Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight)) } - incomingPayment.origin.paymentRequest.paymentSecret != finalPayload.paymentSecret -> { + incomingPayment.paymentRequest.paymentSecret != finalPayload.paymentSecret -> { // BOLT 04: // - if the payment_secret doesn't match the expected value for that payment_hash, // or the payment_secret is required and is not present: @@ -471,15 +429,15 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { logger.warning { "payment with invalid paymentSecret (${finalPayload.paymentSecret})" } Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight)) } - incomingPayment.origin.paymentRequest.amount != null && paymentPart.totalAmount < incomingPayment.origin.paymentRequest.amount -> { + incomingPayment.paymentRequest.amount != null && paymentPart.totalAmount < incomingPayment.paymentRequest.amount -> { // BOLT 04: // - if the amount paid is less than the amount expected: // - MUST fail the HTLC. // - MUST return an incorrect_or_unknown_payment_details error. - logger.warning { "invalid amount (underpayment): ${paymentPart.totalAmount}, expected: ${incomingPayment.origin.paymentRequest.amount}" } + logger.warning { "invalid amount (underpayment): ${paymentPart.totalAmount}, expected: ${incomingPayment.paymentRequest.amount}" } Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight)) } - incomingPayment.origin.paymentRequest.amount != null && paymentPart.totalAmount > incomingPayment.origin.paymentRequest.amount * 2 -> { + incomingPayment.paymentRequest.amount != null && paymentPart.totalAmount > incomingPayment.paymentRequest.amount * 2 -> { // BOLT 04: // - if the amount paid is more than twice the amount expected: // - SHOULD fail the HTLC. @@ -487,11 +445,11 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { // // Note: this allows the origin node to reduce information leakage by altering // the amount while not allowing for accidental gross overpayment. - logger.warning { "invalid amount (overpayment): ${paymentPart.totalAmount}, expected: ${incomingPayment.origin.paymentRequest.amount}" } + logger.warning { "invalid amount (overpayment): ${paymentPart.totalAmount}, expected: ${incomingPayment.paymentRequest.amount}" } Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight)) } - paymentPart is HtlcPart && paymentPart.htlc.cltvExpiry < minFinalCltvExpiry(nodeParams, paymentPart, incomingPayment.origin, currentBlockHeight) -> { - logger.warning { "payment with expiry too small: ${paymentPart.htlc.cltvExpiry}, min is ${minFinalCltvExpiry(nodeParams, paymentPart, incomingPayment.origin, currentBlockHeight)}" } + paymentPart is HtlcPart && paymentPart.htlc.cltvExpiry < minFinalCltvExpiry(nodeParams, paymentPart, incomingPayment, currentBlockHeight) -> { + logger.warning { "payment with expiry too small: ${paymentPart.htlc.cltvExpiry}, min is ${minFinalCltvExpiry(nodeParams, paymentPart, incomingPayment, currentBlockHeight)}" } Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight)) } else -> Either.Right(incomingPayment) @@ -505,10 +463,10 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { Either.Left(rejectPaymentPart(privateKey, paymentPart, null, currentBlockHeight)) } else -> { - val incomingPayment = db.getIncomingPayment(paymentPart.paymentHash) ?: IncomingPayment(metadata.preimage, IncomingPayment.Origin.Offer(metadata), null) + val incomingPayment = db.getLightningIncomingPayment(paymentPart.paymentHash) ?: Bolt12IncomingPayment(metadata.preimage, metadata) when { - incomingPayment.origin !is IncomingPayment.Origin.Offer -> { - logger.warning { "unsupported payment type: ${incomingPayment.origin::class}" } + incomingPayment !is Bolt12IncomingPayment -> { + logger.warning { "unsupported payment type: ${incomingPayment::class}" } Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight)) } paymentPart.paymentHash != metadata.paymentHash -> { @@ -519,11 +477,11 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { logger.warning { "invalid amount (underpayment): ${paymentPart.totalAmount}, expected: ${metadata.amount}" } Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight)) } - paymentPart is HtlcPart && paymentPart.htlc.cltvExpiry < minFinalCltvExpiry(nodeParams, paymentPart, incomingPayment.origin, currentBlockHeight) -> { - logger.warning { "payment with expiry too small: ${paymentPart.htlc.cltvExpiry}, min is ${minFinalCltvExpiry(nodeParams, paymentPart, incomingPayment.origin, currentBlockHeight)}" } + paymentPart is HtlcPart && paymentPart.htlc.cltvExpiry < minFinalCltvExpiry(nodeParams, paymentPart, incomingPayment, currentBlockHeight) -> { + logger.warning { "payment with expiry too small: ${paymentPart.htlc.cltvExpiry}, min is ${minFinalCltvExpiry(nodeParams, paymentPart, incomingPayment, currentBlockHeight)}" } Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight)) } - metadata.createdAtMillis + nodeParams.bolt12InvoiceExpiry.inWholeMilliseconds < currentTimestampMillis() && incomingPayment.received == null -> { + metadata.createdAtMillis + nodeParams.bolt12InvoiceExpiry.inWholeMilliseconds < currentTimestampMillis() && incomingPayment.parts.isEmpty() -> { logger.warning { "the invoice is expired" } Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight)) } @@ -568,9 +526,9 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { * @return number of invoices purged */ suspend fun purgeExpiredPayments(fromCreatedAt: Long = 0, toCreatedAt: Long = currentTimestampMillis()): Int { - return db.listExpiredPayments(fromCreatedAt, toCreatedAt).count { + return db.listLightningExpiredPayments(fromCreatedAt, toCreatedAt).count { logger.info { "purging unpaid expired payment for paymentHash=${it.paymentHash} from DB" } - db.removeIncomingPayment(it.paymentHash) + db.removeLightningIncomingPayment(it.paymentHash) } } @@ -602,7 +560,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { } } - private fun rejectPaymentPart(privateKey: PrivateKey, paymentPart: PaymentPart, incomingPayment: IncomingPayment?, currentBlockHeight: Int): ProcessAddResult.Rejected { + private fun rejectPaymentPart(privateKey: PrivateKey, paymentPart: PaymentPart, incomingPayment: LightningIncomingPayment?, currentBlockHeight: Int): ProcessAddResult.Rejected { val failureMsg = when (paymentPart.finalPayload) { is PaymentOnion.FinalPayload.Blinded -> InvalidOnionBlinding(Sphinx.hash(paymentPart.onionPacket)) is PaymentOnion.FinalPayload.Standard -> IncorrectOrUnknownPaymentDetails(paymentPart.totalAmount, currentBlockHeight.toLong()) @@ -627,10 +585,10 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) { return SendOnTheFlyFundingMessage(msg) } - private fun minFinalCltvExpiry(nodeParams: NodeParams, paymentPart: PaymentPart, origin: IncomingPayment.Origin, currentBlockHeight: Int): CltvExpiry { - val minFinalExpiryDelta = when (origin) { - is IncomingPayment.Origin.Invoice -> origin.paymentRequest.minFinalExpiryDelta ?: Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA - else -> nodeParams.minFinalCltvExpiryDelta + private fun minFinalCltvExpiry(nodeParams: NodeParams, paymentPart: PaymentPart, incomingPayment: LightningIncomingPayment, currentBlockHeight: Int): CltvExpiry { + val minFinalExpiryDelta = when (incomingPayment) { + is Bolt11IncomingPayment -> incomingPayment.paymentRequest.minFinalExpiryDelta ?: Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA + is Bolt12IncomingPayment -> nodeParams.minFinalCltvExpiryDelta } return when { paymentPart is HtlcPart && paymentPart.htlc.usesOnTheFlyFunding -> { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/db/InMemoryPaymentsDb.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/db/InMemoryPaymentsDb.kt index f20ab9337..c812f495f 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/db/InMemoryPaymentsDb.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/db/InMemoryPaymentsDb.kt @@ -1,40 +1,38 @@ package fr.acinq.lightning.db import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto import fr.acinq.bitcoin.TxId import fr.acinq.lightning.payment.FinalFailure import fr.acinq.lightning.utils.UUID -import fr.acinq.lightning.utils.toByteVector32 class InMemoryPaymentsDb : PaymentsDb { - private val incoming = mutableMapOf() + private val incoming = mutableMapOf() + private val onchainIncoming = mutableMapOf() private val outgoing = mutableMapOf() private val onChainOutgoing = mutableMapOf() private val outgoingParts = mutableMapOf>() override suspend fun setLocked(txId: TxId) {} - override suspend fun addIncomingPayment(preimage: ByteVector32, origin: IncomingPayment.Origin, createdAt: Long): IncomingPayment { - val paymentHash = Crypto.sha256(preimage).toByteVector32() - require(!incoming.contains(paymentHash)) { "an incoming payment for $paymentHash already exists" } - val incomingPayment = IncomingPayment(preimage, origin, null, createdAt) - incoming[paymentHash] = incomingPayment - return incomingPayment + override suspend fun addIncomingPayment(incomingPayment: IncomingPayment) { + when (incomingPayment) { + is LightningIncomingPayment -> { + require(!incoming.contains(incomingPayment.paymentHash)) { "an incoming payment for ${incomingPayment.paymentHash} already exists" } + incoming[incomingPayment.paymentHash] = incomingPayment + } + is OnChainIncomingPayment -> { + require(!onchainIncoming.contains(incomingPayment.id)) { "an incoming payment with id=${incomingPayment.id} already exists" } + onchainIncoming[incomingPayment.id] = incomingPayment + } + else -> TODO() + } } - override suspend fun getIncomingPayment(paymentHash: ByteVector32): IncomingPayment? = incoming[paymentHash] + override suspend fun getLightningIncomingPayment(paymentHash: ByteVector32): LightningIncomingPayment? = incoming[paymentHash] - override suspend fun receivePayment(paymentHash: ByteVector32, receivedWith: List, receivedAt: Long) { + override suspend fun receiveLightningPayment(paymentHash: ByteVector32, parts: List) { when (val payment = incoming[paymentHash]) { null -> Unit // no-op - else -> incoming[paymentHash] = run { - payment.copy( - received = IncomingPayment.Received( - receivedWith = (payment.received?.receivedWith ?: emptySet()) + receivedWith, - receivedAt = receivedAt - ) - ) - } + else -> incoming[paymentHash] = payment.addReceivedParts(parts) } } @@ -46,19 +44,19 @@ class InMemoryPaymentsDb : PaymentsDb { .take(count) .toList() - override suspend fun listExpiredPayments(fromCreatedAt: Long, toCreatedAt: Long): List = + override suspend fun listLightningExpiredPayments(fromCreatedAt: Long, toCreatedAt: Long): List = incoming.values .asSequence() .filter { it.createdAt in fromCreatedAt until toCreatedAt } .filter { it.isExpired() } - .filter { it.received == null } + .filter { it.parts.isEmpty() } .sortedByDescending { it.createdAt } .toList() - override suspend fun removeIncomingPayment(paymentHash: ByteVector32): Boolean { - val payment = getIncomingPayment(paymentHash) - return when (payment?.received) { - null -> incoming.remove(paymentHash) != null + override suspend fun removeLightningIncomingPayment(paymentHash: ByteVector32): Boolean { + val payment = getLightningIncomingPayment(paymentHash) + return when (payment?.parts?.isEmpty()) { + true -> incoming.remove(paymentHash) != null else -> false // do nothing if payment already partially paid } } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/db/PaymentsDbTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/db/PaymentsDbTestsCommon.kt index 356cadace..1c98b08e3 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/db/PaymentsDbTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/db/PaymentsDbTestsCommon.kt @@ -18,49 +18,49 @@ class PaymentsDbTestsCommon : LightningTestSuite() { @Test fun `receive incoming lightning payment with 1 htlc`() = runSuspendTest { val (db, preimage, pr) = createFixture() - assertNull(db.getIncomingPayment(pr.paymentHash)) + assertNull(db.getLightningIncomingPayment(pr.paymentHash)) val channelId = randomBytes32() - val incoming = IncomingPayment(preimage, IncomingPayment.Origin.Invoice(pr), null, 100) - db.addIncomingPayment(preimage, IncomingPayment.Origin.Invoice(pr), 100) - val pending = db.getIncomingPayment(pr.paymentHash) - assertNotNull(pending) + val incoming = Bolt11IncomingPayment(preimage, pr, createdAt = 100) + db.addIncomingPayment(incoming) + val pending = db.getLightningIncomingPayment(pr.paymentHash) + assertIs(pending) assertEquals(incoming, pending) - val receivedWith = IncomingPayment.ReceivedWith.LightningPayment(200_000.msat, channelId, 1, fundingFee = null) - db.receivePayment(pr.paymentHash, listOf(receivedWith), 110) - val received = db.getIncomingPayment(pr.paymentHash) + val parts = LightningIncomingPayment.Part.Htlc(200_000.msat, channelId, 1, fundingFee = null, receivedAt = 110) + db.receiveLightningPayment(pr.paymentHash, listOf(parts)) + val received = db.getLightningIncomingPayment(pr.paymentHash) assertNotNull(received) - assertEquals(pending.copy(received = IncomingPayment.Received(listOf(receivedWith), 110)), received) + assertEquals(pending.addReceivedParts(listOf(parts)), received) } @Test fun `receive incoming lightning payment with several parts`() = runSuspendTest { val (db, preimage, pr) = createFixture() - assertNull(db.getIncomingPayment(pr.paymentHash)) + assertNull(db.getLightningIncomingPayment(pr.paymentHash)) val (channelId1, channelId2) = listOf(randomBytes32(), randomBytes32()) - val incoming = IncomingPayment(preimage, IncomingPayment.Origin.Invoice(pr), null, 200) - db.addIncomingPayment(preimage, IncomingPayment.Origin.Invoice(pr), 200) - val pending = db.getIncomingPayment(pr.paymentHash) - assertNotNull(pending) + val incoming = Bolt11IncomingPayment(preimage, pr, createdAt = 200) + db.addIncomingPayment(incoming) + val pending = db.getLightningIncomingPayment(pr.paymentHash) + assertIs(pending) assertEquals(incoming, pending) - db.receivePayment( + db.receiveLightningPayment( pr.paymentHash, listOf( - IncomingPayment.ReceivedWith.LightningPayment(57_000.msat, channelId1, 1, fundingFee = null), - IncomingPayment.ReceivedWith.LightningPayment(43_000.msat, channelId2, 54, fundingFee = null), - ), 110 + LightningIncomingPayment.Part.Htlc(57_000.msat, channelId1, 1, fundingFee = null, receivedAt = 110), + LightningIncomingPayment.Part.Htlc(43_000.msat, channelId2, 54, fundingFee = null, receivedAt = 110), + ) ) - val received = db.getIncomingPayment(pr.paymentHash) + val received = db.getLightningIncomingPayment(pr.paymentHash) assertNotNull(received) assertEquals(100_000.msat, received.amount) assertEquals(0.msat, received.fees) - assertEquals(2, received.received!!.receivedWith.size) - assertEquals(57_000.msat, received.received!!.receivedWith.elementAt(0).amountReceived) - assertEquals(0.msat, received.received!!.receivedWith.elementAt(0).fees) - assertEquals(channelId1, (received.received!!.receivedWith.elementAt(0) as IncomingPayment.ReceivedWith.LightningPayment).channelId) - assertEquals(54, (received.received!!.receivedWith.elementAt(1) as IncomingPayment.ReceivedWith.LightningPayment).htlcId) + assertEquals(2, received.parts.size) + assertEquals(57_000.msat, received.parts.elementAt(0).amountReceived) + assertEquals(0.msat, received.parts.elementAt(0).fees) + assertEquals(channelId1, (received.parts.elementAt(0) as LightningIncomingPayment.Part.Htlc).channelId) + assertEquals(54, (received.parts.elementAt(1) as LightningIncomingPayment.Part.Htlc).htlcId) } @Test @@ -68,80 +68,55 @@ class PaymentsDbTestsCommon : LightningTestSuite() { val (db, preimage, pr) = createFixture() val channelId = randomBytes32() - db.addIncomingPayment(preimage, IncomingPayment.Origin.Invoice(pr), 200) - val receivedWith = listOf( - IncomingPayment.ReceivedWith.LightningPayment(200_000.msat, channelId, 1, fundingFee = null), - IncomingPayment.ReceivedWith.LightningPayment(100_000.msat, channelId, 2, fundingFee = null) + val incoming = Bolt11IncomingPayment(preimage, pr, createdAt = 200) + db.addIncomingPayment(incoming) + val parts = listOf( + LightningIncomingPayment.Part.Htlc(200_000.msat, channelId, 1, fundingFee = null, receivedAt = 110), + LightningIncomingPayment.Part.Htlc(100_000.msat, channelId, 2, fundingFee = null, receivedAt = 150) ) - db.receivePayment(pr.paymentHash, listOf(receivedWith.first()), 110) - val received1 = db.getIncomingPayment(pr.paymentHash) + db.receiveLightningPayment(pr.paymentHash, listOf(parts.first())) + val received1 = db.getLightningIncomingPayment(pr.paymentHash) assertNotNull(received1) - assertNotNull(received1.received) assertEquals(200_000.msat, received1.amount) - db.receivePayment(pr.paymentHash, listOf(receivedWith.last()), 150) - val received2 = db.getIncomingPayment(pr.paymentHash) + db.receiveLightningPayment(pr.paymentHash, listOf(parts.last())) + val received2 = db.getLightningIncomingPayment(pr.paymentHash) assertNotNull(received2) - assertNotNull(received2.received) assertEquals(300_000.msat, received2.amount) - assertEquals(150, received2.received!!.receivedAt) - assertEquals(receivedWith, received2.received!!.receivedWith) + assertEquals(150, received2.completedAt) + assertEquals(parts, received2.parts) } @Test fun `receive lightning payment with funding fee`() = runSuspendTest { val (db, preimage, pr) = createFixture() - db.addIncomingPayment(preimage, IncomingPayment.Origin.Invoice(pr), 200) - val receivedWith = IncomingPayment.ReceivedWith.LightningPayment(40_000_000.msat, randomBytes32(), 3, LiquidityAds.FundingFee(10_000_000.msat, TxId(randomBytes32()))) - db.receivePayment(pr.paymentHash, listOf(receivedWith), 110) - val received = db.getIncomingPayment(pr.paymentHash) - assertNotNull(received?.received) + val incoming = Bolt11IncomingPayment(preimage, pr, createdAt = 200) + db.addIncomingPayment(incoming) + val parts = LightningIncomingPayment.Part.Htlc(40_000_000.msat, randomBytes32(), 3, LiquidityAds.FundingFee(10_000_000.msat, TxId(randomBytes32())), receivedAt = 110) + db.receiveLightningPayment(pr.paymentHash, listOf(parts)) + val received = db.getLightningIncomingPayment(pr.paymentHash) assertEquals(40_000_000.msat, received!!.amount) assertEquals(10_000_000.msat, received.fees) } - @Test - fun `receive incoming on-chain payments`() = runSuspendTest { - val (db, _, _) = createFixture() - val origin = IncomingPayment.Origin.OnChain(TxId(randomBytes32()), setOf(OutPoint(TxId(randomBytes32()), 7))) - run { - val incomingPayment = db.addIncomingPayment(randomBytes32(), origin) - val receivedWith = IncomingPayment.ReceivedWith.NewChannel(100_000_000.msat, 5_000_000.msat, 2_500.sat, randomBytes32(), origin.txId, null, null) - db.receivePayment(incomingPayment.paymentHash, listOf(receivedWith)) - val received = db.getIncomingPayment(incomingPayment.paymentHash) - assertNotNull(received?.received) - assertEquals(100_000_000.msat, received!!.amount) - assertEquals(7_500_000.msat, received.fees) - } - run { - val incomingPayment = db.addIncomingPayment(randomBytes32(), origin) - val receivedWith = IncomingPayment.ReceivedWith.SpliceIn(100_000_000.msat, 5_000_000.msat, 2_500.sat, randomBytes32(), origin.txId, null, null) - db.receivePayment(incomingPayment.paymentHash, listOf(receivedWith)) - val received = db.getIncomingPayment(incomingPayment.paymentHash) - assertNotNull(received?.received) - assertEquals(100_000_000.msat, received!!.amount) - assertEquals(7_500_000.msat, received.fees) - } - } - @Test fun `reject duplicate payment hash`() = runSuspendTest { val (db, preimage, pr) = createFixture() - db.addIncomingPayment(preimage, IncomingPayment.Origin.Invoice(pr)) - assertFails { db.addIncomingPayment(preimage, IncomingPayment.Origin.Invoice(pr)) } + db.addIncomingPayment(Bolt11IncomingPayment(preimage, pr)) + assertFails { db.addIncomingPayment(Bolt11IncomingPayment(preimage, pr)) } } @Test fun `set expired invoices`() = runSuspendTest { val (db, preimage, _) = createFixture() val pr = createExpiredInvoice(preimage) - db.addIncomingPayment(preimage, IncomingPayment.Origin.Invoice(pr)) + db.addIncomingPayment(Bolt11IncomingPayment(preimage, pr)) - val expired = db.getIncomingPayment(pr.paymentHash) - assertNotNull(expired) + val expired = db.getLightningIncomingPayment(pr.paymentHash) + assertIs(expired) assertTrue(expired.isExpired()) - assertEquals(IncomingPayment.Origin.Invoice(pr), expired.origin) - assertEquals(preimage, expired.preimage) + assertEquals(pr, expired.paymentRequest) + assertEquals(preimage, expired.paymentPreimage) } @Test diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt index 5bcba2e0d..c1d5a27b4 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt @@ -533,7 +533,7 @@ class PeerTest : LightningTestSuite() { alice.forward(bob2alice3.expect(), connectionId = 2) bob.forward(alice2bob3.expect(), connectionId = 2) - assertEquals(invoice.amount, alice.db.payments.getIncomingPayment(invoice.paymentHash)?.received?.amount) + assertEquals(invoice.amount, alice.db.payments.getLightningIncomingPayment(invoice.paymentHash)?.amount) } @Test diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt index 0e2da9fe4..913d1ee1f 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt @@ -9,10 +9,7 @@ import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.channel.* import fr.acinq.lightning.crypto.RouteBlinding import fr.acinq.lightning.crypto.sphinx.Sphinx.hash -import fr.acinq.lightning.db.InMemoryPaymentsDb -import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment -import fr.acinq.lightning.db.IncomingPayment -import fr.acinq.lightning.db.IncomingPaymentsDb +import fr.acinq.lightning.db.* import fr.acinq.lightning.io.AddLiquidityForIncomingPayment import fr.acinq.lightning.io.SendOnTheFlyFundingMessage import fr.acinq.lightning.io.WrappedChannelCommand @@ -143,12 +140,12 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) - val expected = ChannelCommand.Htlc.Settlement.Fulfill(add.id, incomingPayment.preimage, commit = true) + val expected = ChannelCommand.Htlc.Settlement.Fulfill(add.id, incomingPayment.paymentPreimage, commit = true) assertEquals(setOf(WrappedChannelCommand(add.channelId, expected)), result.actions.toSet()) - assertEquals(result.incomingPayment.received, result.received) - assertEquals(defaultAmount, result.received.amount) - assertEquals(listOf(IncomingPayment.ReceivedWith.LightningPayment(defaultAmount, channelId, 12, null)), result.received.receivedWith) + assertEqualsIgnoreTimestamps(result.incomingPayment.parts, result.parts) + assertEquals(defaultAmount, result.amount) + assertEqualsIgnoreTimestamps(listOf(LightningIncomingPayment.Part.Htlc(defaultAmount, channelId, 12, null)), result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } @@ -176,15 +173,15 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val add = makeUpdateAddHtlc(5, channelId, paymentHandler, incomingPayment.paymentHash, makeMppPayload(amount2, totalAmount, paymentSecret)) val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) - val (expectedActions, expectedReceivedWith) = setOf( + val (expectedActions, parts) = setOf( // @formatter:off - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, defaultPreimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount1, channelId, 0, fundingFee = null), - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(5, defaultPreimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount2, channelId, 5, fundingFee = null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, defaultPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount1, channelId, 0, fundingFee = null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(5, defaultPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount2, channelId, 5, fundingFee = null), // @formatter:on ).unzip() assertEquals(expectedActions.toSet(), result.actions.toSet()) - assertEquals(totalAmount, result.received.amount) - assertEquals(expectedReceivedWith, result.received.receivedWith) + assertEquals(totalAmount, result.amount) + assertEqualsIgnoreTimestamps(parts, result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } } @@ -201,7 +198,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val add = makeUpdateAddHtlc(0, channelId, paymentHandler, incomingPayment.paymentHash, makeMppPayload(amount1, totalAmount, paymentSecret)) val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) - assertNull(result.incomingPayment.received) + assertTrue(result.incomingPayment.parts.isEmpty()) assertTrue(result.actions.isEmpty()) add } @@ -213,7 +210,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { run { val result = paymentHandler.process(add1, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) - assertNull(result.incomingPayment.received) + assertTrue(result.incomingPayment.parts.isEmpty()) assertTrue(result.actions.isEmpty()) } @@ -222,15 +219,15 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val add = makeUpdateAddHtlc(1, channelId, paymentHandler, incomingPayment.paymentHash, makeMppPayload(amount2, totalAmount, paymentSecret)) val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) - val (expectedActions, expectedReceivedWith) = setOf( + val (expectedActions, parts) = setOf( // @formatter:off - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, defaultPreimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount1, channelId, 0, fundingFee = null), - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, defaultPreimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount2, channelId, 1, fundingFee = null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, defaultPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount1, channelId, 0, fundingFee = null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, defaultPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount2, channelId, 1, fundingFee = null), // @formatter:on ).unzip() assertEquals(expectedActions.toSet(), result.actions.toSet()) - assertEquals(totalAmount, result.received.amount) - assertEquals(expectedReceivedWith, result.received.receivedWith) + assertEquals(totalAmount, result.amount) + assertEqualsIgnoreTimestamps(parts, result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } } @@ -245,13 +242,13 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(1, result.actions.size) val addLiquidity = result.actions.first() assertIs(addLiquidity) - assertEquals(incomingPayment.preimage, addLiquidity.preimage) + assertEquals(incomingPayment.paymentPreimage, addLiquidity.preimage) assertEquals(defaultAmount, addLiquidity.paymentAmount) assertEquals(defaultAmount, addLiquidity.requestedAmount.toMilliSatoshi()) assertEquals(TestConstants.fundingRates.fundingRates.first(), addLiquidity.fundingRate) assertEquals(listOf(willAddHtlc), addLiquidity.willAddHtlcs) // We don't update the payments DB: we're waiting to receive HTLCs after the open/splice. - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } @Test @@ -268,7 +265,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertIs(addLiquidity) assertEquals(555_556.sat, addLiquidity.requestedAmount) // We don't update the payments DB: we're waiting to receive HTLCs after the open/splice. - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } @Test @@ -296,10 +293,10 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertIs(result) assertEquals(1, result.actions.size) val addLiquidity = result.actions.first() as AddLiquidityForIncomingPayment - assertEquals(incomingPayment.preimage, addLiquidity.preimage) + assertEquals(incomingPayment.paymentPreimage, addLiquidity.preimage) assertEquals(amount * 2, addLiquidity.paymentAmount) assertEquals(2, addLiquidity.willAddHtlcs.size) - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } } @@ -330,10 +327,10 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertIs(result) assertEquals(1, result.actions.size) val addLiquidity = result.actions.first() as AddLiquidityForIncomingPayment - assertEquals(incomingPayment.preimage, addLiquidity.preimage) + assertEquals(incomingPayment.paymentPreimage, addLiquidity.preimage) assertEquals(totalAmount, addLiquidity.paymentAmount) assertEquals(2, addLiquidity.willAddHtlcs.size) - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } } @@ -358,11 +355,11 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(1, result.actions.size) val addLiquidity = result.actions.first() assertIs(addLiquidity) - assertEquals(incomingPayment.preimage, addLiquidity.preimage) + assertEquals(incomingPayment.paymentPreimage, addLiquidity.preimage) assertEquals(defaultAmount, addLiquidity.paymentAmount) assertEquals(defaultAmount, addLiquidity.requestedAmount.toMilliSatoshi()) assertEquals(TestConstants.fundingRates.fundingRates.first(), addLiquidity.fundingRate) - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } @Test @@ -490,10 +487,10 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertIs(result) assertEquals(1, result.actions.size) val addLiquidity = result.actions.first() as AddLiquidityForIncomingPayment - assertEquals(incomingPayment.preimage, addLiquidity.preimage) + assertEquals(incomingPayment.paymentPreimage, addLiquidity.preimage) assertEquals(amount2.truncateToSatoshi(), addLiquidity.requestedAmount) assertEquals(totalAmount, addLiquidity.paymentAmount) - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } // Step 3 of 3: @@ -503,15 +500,15 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val htlc = makeUpdateAddHtlc(1, channelId, paymentHandler, incomingPayment.paymentHash, makeMppPayload(amount2, totalAmount, paymentSecret)) val result = paymentHandler.process(htlc, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, TestConstants.fundingRates) assertIs(result) - val (expectedActions, expectedReceivedWith) = setOf( + val (expectedActions, parts) = setOf( // @formatter:off - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, incomingPayment.preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount1, channelId, 0, fundingFee = null), - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, incomingPayment.preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount2, channelId, 1, fundingFee = null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, incomingPayment.paymentPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount1, channelId, 0, fundingFee = null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, incomingPayment.paymentPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount2, channelId, 1, fundingFee = null), // @formatter:on ).unzip() assertEquals(expectedActions.toSet(), result.actions.toSet()) - assertEquals(totalAmount, result.received.amount) - assertEquals(expectedReceivedWith, result.received.receivedWith) + assertEquals(totalAmount, result.amount) + assertEqualsIgnoreTimestamps(parts, result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } } @@ -547,7 +544,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertIs(willFailHtlc).also { assertEquals(willAddHtlc.id, it.id) } val failHtlc = ChannelCommand.Htlc.Settlement.Fail(0, ChannelCommand.Htlc.Settlement.Fail.Reason.Failure(TemporaryNodeFailure), commit = true) assertTrue(result.actions.contains(WrappedChannelCommand(channelId, failHtlc))) - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } // Step 3 of 4: @@ -567,15 +564,15 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val htlc = makeUpdateAddHtlc(2, channelId, paymentHandler, incomingPayment.paymentHash, makeMppPayload(amount2, totalAmount, paymentSecret)) val result = paymentHandler.process(htlc, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, TestConstants.fundingRates) assertIs(result) - val (expectedActions, expectedReceivedWith) = setOf( + val (expectedActions, parts) = setOf( // @formatter:off - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, incomingPayment.preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount1, channelId, 1, fundingFee = null), - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(2, incomingPayment.preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount2, channelId, 2, fundingFee = null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, incomingPayment.paymentPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount1, channelId, 1, fundingFee = null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(2, incomingPayment.paymentPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount2, channelId, 2, fundingFee = null), // @formatter:on ).unzip() assertEquals(expectedActions.toSet(), result.actions.toSet()) - assertEquals(totalAmount, result.received.amount) - assertEquals(expectedReceivedWith, result.received.receivedWith) + assertEquals(totalAmount, result.amount) + assertEqualsIgnoreTimestamps(parts, result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } } @@ -613,7 +610,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(willAddHtlcs.map { it.id }.toSet(), willFailHtlcs.map { it.id }.toSet()) val failHtlc = ChannelCommand.Htlc.Settlement.Fail(htlc.id, ChannelCommand.Htlc.Settlement.Fail.Reason.Failure(TemporaryNodeFailure), commit = true) assertTrue(result.actions.contains(WrappedChannelCommand(channelId, failHtlc))) - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } @Test @@ -635,9 +632,9 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { when (failure) { null -> { assertIs(result) - assertEquals(listOf(SendOnTheFlyFundingMessage(AddFeeCredit(paymentHandler.nodeParams.chainHash, incomingPayment.preimage))), result.actions) - assertEquals(totalAmount, result.received.amount) - assertEquals(listOf(IncomingPayment.ReceivedWith.AddedToFeeCredit(totalAmount)), result.received.receivedWith) + assertEquals(listOf(SendOnTheFlyFundingMessage(AddFeeCredit(paymentHandler.nodeParams.chainHash, incomingPayment.paymentPreimage))), result.actions) + assertEquals(totalAmount, result.amount) + assertEqualsIgnoreTimestamps(listOf(LightningIncomingPayment.Part.FeeCredit(totalAmount)), result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } else -> { @@ -678,15 +675,15 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val willAddHtlc = makeWillAddHtlc(paymentHandler, incomingPayment.paymentHash, makeMppPayload(amount2, totalAmount, paymentSecret)) val result = paymentHandler.process(willAddHtlc, feeCreditFeatures, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, TestConstants.fundingRates, currentFeeCredit = 0.msat) assertIs(result) - val (expectedActions, expectedReceivedWith) = setOf( + val (expectedActions, parts) = setOf( // @formatter:off - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, incomingPayment.preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount1, channelId, 0, fundingFee = null), - SendOnTheFlyFundingMessage(AddFeeCredit(paymentHandler.nodeParams.chainHash, incomingPayment.preimage)) to IncomingPayment.ReceivedWith.AddedToFeeCredit(amount2), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, incomingPayment.paymentPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount1, channelId, 0, fundingFee = null), + SendOnTheFlyFundingMessage(AddFeeCredit(paymentHandler.nodeParams.chainHash, incomingPayment.paymentPreimage)) to LightningIncomingPayment.Part.FeeCredit(amount2), // @formatter:on ).unzip() assertEquals(expectedActions.toSet(), result.actions.toSet()) - assertEquals(totalAmount, result.received.amount) - assertEquals(expectedReceivedWith, result.received.receivedWith) + assertEquals(totalAmount, result.amount) + assertEqualsIgnoreTimestamps(parts, result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } } @@ -737,7 +734,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(totalAmount, addLiquidity.paymentAmount) assertEquals(105_000.sat, addLiquidity.requestedAmount) // We don't update the payments DB: we're waiting to receive HTLCs after the open/splice. - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } } @@ -756,7 +753,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(totalAmount, addLiquidity.paymentAmount) assertEquals(100_001.sat, addLiquidity.requestedAmount) // We don't update the payments DB: we're waiting to receive HTLCs after the open/splice. - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } @Test @@ -778,7 +775,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val addFeeCredit = result.actions.first() assertIs(addFeeCredit) assertIs(addFeeCredit.message) - assertEquals(amount, result.received.amount) + assertEquals(amount, result.amount) checkDbPayment(result.incomingPayment, paymentHandler.db) } @@ -797,7 +794,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(amount, addLiquidity.paymentAmount) assertEquals(104_000.sat, addLiquidity.requestedAmount) // We don't update the payments DB: we're waiting to receive HTLCs after the open/splice. - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } } @@ -815,7 +812,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(totalAmount, addLiquidity.paymentAmount) assertEquals(110_000.sat, addLiquidity.requestedAmount) // We don't update the payments DB: we're waiting to receive HTLCs after the open/splice. - assertNull(paymentHandler.db.getIncomingPayment(incomingPayment.paymentHash)?.received) + assertEquals(emptyList(), paymentHandler.db.getLightningIncomingPayment(incomingPayment.paymentHash)?.parts) } @Test @@ -878,15 +875,15 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertTrue(htlc.amountMsat < amount2) val result = paymentHandler.process(htlc, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, TestConstants.fundingRates) assertIs(result) - val (expectedActions, expectedReceivedWith) = setOf( + val (expectedActions, parts) = setOf( // @formatter:off - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, incomingPayment.preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount1, channelId, 0, fundingFee = null), - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, incomingPayment.preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount2 - purchase.fundingFee.amount, channelId, 1, purchase.fundingFee), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, incomingPayment.paymentPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount1, channelId, 0, fundingFee = null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, incomingPayment.paymentPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount2 - purchase.fundingFee.amount, channelId, 1, purchase.fundingFee), // @formatter:on ).unzip() assertEquals(expectedActions.toSet(), result.actions.toSet()) - assertEquals(totalAmount - purchase.fundingFee.amount, result.received.amount) - assertEquals(expectedReceivedWith, result.received.receivedWith) + assertEquals(totalAmount - purchase.fundingFee.amount, result.amount) + assertEqualsIgnoreTimestamps(parts, result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } } @@ -930,14 +927,14 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val currentBlockHeight = TestConstants.defaultBlockHeight + 24 val result = paymentHandler.process(htlc, Features.empty, currentBlockHeight, TestConstants.feeratePerKw, TestConstants.fundingRates) assertIs(result) - val (expectedActions, expectedReceivedWith) = setOf( + val (expectedActions, parts) = setOf( // @formatter:off - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(7, incomingPayment.preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount, channelId, 7, fundingFee), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(7, incomingPayment.paymentPreimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount, channelId, 7, fundingFee), // @formatter:on ).unzip() assertEquals(expectedActions.toSet(), result.actions.toSet()) - assertEquals(amount, result.received.amount) - assertEquals(expectedReceivedWith, result.received.receivedWith) + assertEquals(amount, result.amount) + assertEqualsIgnoreTimestamps(parts, result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } } @@ -985,9 +982,9 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val add = makeUpdateAddHtlc(1, channelId, paymentHandler, incomingPayment.paymentHash, makeMppPayload(defaultAmount, defaultAmount, paymentSecret), fundingFee = payment.fundingFee) val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, TestConstants.fundingRates) assertIs(result) - assertEquals(listOf(WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, incomingPayment.preimage, commit = true))), result.actions) - assertEquals(defaultAmount - payment.fundingFee.amount, result.received.amount) - assertEquals(listOf(IncomingPayment.ReceivedWith.LightningPayment(defaultAmount - payment.fundingFee.amount, channelId, 1, payment.fundingFee)), result.received.receivedWith) + assertEquals(listOf(WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, incomingPayment.paymentPreimage, commit = true))), result.actions) + assertEquals(defaultAmount - payment.fundingFee.amount, result.amount) + assertEqualsIgnoreTimestamps(listOf(LightningIncomingPayment.Part.Htlc(defaultAmount - payment.fundingFee.amount, channelId, 1, payment.fundingFee)), result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } } @@ -1062,8 +1059,8 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) val expected = setOf( - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(7, incomingPayment.preimage, commit = true)), - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(11, incomingPayment.preimage, commit = true)), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(7, incomingPayment.paymentPreimage, commit = true)), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(11, incomingPayment.paymentPreimage, commit = true)), ) assertEquals(expected, result.actions.toSet()) } @@ -1100,9 +1097,9 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val result = paymentHandler.process(add3, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) val expected = setOf( - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(3, incomingPayment.preimage, commit = true)), - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(5, incomingPayment.preimage, commit = true)), - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(6, incomingPayment.preimage, commit = true)) + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(3, incomingPayment.paymentPreimage, commit = true)), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(5, incomingPayment.paymentPreimage, commit = true)), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(6, incomingPayment.paymentPreimage, commit = true)) ) assertEquals(expected, result.actions.toSet()) } @@ -1115,7 +1112,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val addGreaterExpiry = add.copy(cltvExpiry = add.cltvExpiry + CltvExpiryDelta(6)) val result = paymentHandler.process(addGreaterExpiry, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) - val expected = WrappedChannelCommand(add.channelId, ChannelCommand.Htlc.Settlement.Fulfill(add.id, incomingPayment.preimage, commit = true)) + val expected = WrappedChannelCommand(add.channelId, ChannelCommand.Htlc.Settlement.Fulfill(add.id, incomingPayment.paymentPreimage, commit = true)) assertEquals(setOf(expected), result.actions.toSet()) } @@ -1138,18 +1135,18 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val add2 = makeUpdateAddHtlc(5, randomBytes32(), paymentHandler, incomingPayment.paymentHash, makeMppPayload(defaultAmount / 2, defaultAmount, paymentSecret)) val result2 = paymentHandler.process(add2, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result2) - assertEquals(defaultAmount, result2.received.amount) + assertEquals(defaultAmount, result2.amount) val expected = setOf( - WrappedChannelCommand(add1.channelId, ChannelCommand.Htlc.Settlement.Fulfill(add1.id, incomingPayment.preimage, commit = true)), - WrappedChannelCommand(add2.channelId, ChannelCommand.Htlc.Settlement.Fulfill(add2.id, incomingPayment.preimage, commit = true)) + WrappedChannelCommand(add1.channelId, ChannelCommand.Htlc.Settlement.Fulfill(add1.id, incomingPayment.paymentPreimage, commit = true)), + WrappedChannelCommand(add2.channelId, ChannelCommand.Htlc.Settlement.Fulfill(add2.id, incomingPayment.paymentPreimage, commit = true)) ) assertEquals(expected, result2.actions.toSet()) // The second htlc is reprocessed (e.g. because our peer disconnected before we could send them the preimage). val result2b = paymentHandler.process(add2, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result2b) - assertEquals(defaultAmount, result2b.received.amount) - assertEquals(listOf(WrappedChannelCommand(add2.channelId, ChannelCommand.Htlc.Settlement.Fulfill(add2.id, incomingPayment.preimage, commit = true))), result2b.actions) + assertEquals(defaultAmount, result2b.amount) + assertEquals(listOf(WrappedChannelCommand(add2.channelId, ChannelCommand.Htlc.Settlement.Fulfill(add2.id, incomingPayment.paymentPreimage, commit = true))), result2b.actions) } @Test @@ -1411,8 +1408,8 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) val expected = setOf( - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(3, incomingPayment.preimage, commit = true)), - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(4, incomingPayment.preimage, commit = true)), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(3, incomingPayment.paymentPreimage, commit = true)), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(4, incomingPayment.paymentPreimage, commit = true)), ) assertEquals(expected, result.actions.toSet()) } @@ -1440,8 +1437,8 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertIs(result2) val expected = setOf( - WrappedChannelCommand(channelId1, ChannelCommand.Htlc.Settlement.Fulfill(htlc1.id, incomingPayment.preimage, commit = true)), - WrappedChannelCommand(channelId2, ChannelCommand.Htlc.Settlement.Fulfill(htlc2.id, incomingPayment.preimage, commit = true)), + WrappedChannelCommand(channelId1, ChannelCommand.Htlc.Settlement.Fulfill(htlc1.id, incomingPayment.paymentPreimage, commit = true)), + WrappedChannelCommand(channelId2, ChannelCommand.Htlc.Settlement.Fulfill(htlc2.id, incomingPayment.paymentPreimage, commit = true)), ) assertEquals(expected, result2.actions.toSet()) } @@ -1451,7 +1448,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { run { val result = paymentHandler.process(htlc1, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) - val expected = WrappedChannelCommand(channelId1, ChannelCommand.Htlc.Settlement.Fulfill(htlc1.id, incomingPayment.preimage, commit = true)) + val expected = WrappedChannelCommand(channelId1, ChannelCommand.Htlc.Settlement.Fulfill(htlc1.id, incomingPayment.paymentPreimage, commit = true)) assertEquals(setOf(expected), result.actions.toSet()) } } @@ -1478,8 +1475,8 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertIs(result2) val expected = setOf( - WrappedChannelCommand(channelId1, ChannelCommand.Htlc.Settlement.Fulfill(htlc1.id, incomingPayment.preimage, commit = true)), - WrappedChannelCommand(channelId2, ChannelCommand.Htlc.Settlement.Fulfill(htlc2.id, incomingPayment.preimage, commit = true)), + WrappedChannelCommand(channelId1, ChannelCommand.Htlc.Settlement.Fulfill(htlc1.id, incomingPayment.paymentPreimage, commit = true)), + WrappedChannelCommand(channelId2, ChannelCommand.Htlc.Settlement.Fulfill(htlc2.id, incomingPayment.paymentPreimage, commit = true)), ) assertEquals(expected, result2.actions.toSet()) } @@ -1533,36 +1530,33 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { defaultPreimage, defaultAmount, Either.Left("paid"), listOf(), expiry = 1.hours, timestampSeconds = 100 ) - paymentHandler.db.receivePayment( + paymentHandler.db.receiveLightningPayment( paidInvoice.paymentHash, - receivedWith = listOf( - IncomingPayment.ReceivedWith.NewChannel( + parts = listOf( + LightningIncomingPayment.Part.Htlc( amountReceived = 15_000_000.msat, - serviceFee = 1_000_000.msat, - miningFee = 0.sat, channelId = randomBytes32(), - txId = TxId(randomBytes32()), - confirmedAt = null, - lockedAt = null + htlcId = 42, + fundingFee = null, + receivedAt = 101 // simulate incoming payment being paid before it expired ) ), - receivedAt = 101 // simulate incoming payment being paid before it expired ) // create unexpired payment delay(100.milliseconds) val unexpiredInvoice = paymentHandler.createInvoice(randomBytes32(), defaultAmount, Either.Left("unexpired"), listOf(), expiry = 1.hours) - val unexpiredPayment = paymentHandler.db.getIncomingPayment(unexpiredInvoice.paymentHash)!! - val paidPayment = paymentHandler.db.getIncomingPayment(paidInvoice.paymentHash)!! - val expiredPayment = paymentHandler.db.getIncomingPayment(expiredInvoice.paymentHash)!! + val unexpiredPayment = paymentHandler.db.getLightningIncomingPayment(unexpiredInvoice.paymentHash)!! + val paidPayment = paymentHandler.db.getLightningIncomingPayment(paidInvoice.paymentHash)!! + val expiredPayment = paymentHandler.db.getLightningIncomingPayment(expiredInvoice.paymentHash)!! val db = paymentHandler.db assertIs(db) assertEquals(db.listIncomingPayments(5, 0), listOf(unexpiredPayment, paidPayment, expiredPayment)) - assertEquals(db.listExpiredPayments(), listOf(expiredPayment)) + assertEquals(db.listLightningExpiredPayments(), listOf(expiredPayment)) assertEquals(paymentHandler.purgeExpiredPayments(), 1) - assertEquals(db.listExpiredPayments(), emptyList()) + assertEquals(db.listLightningExpiredPayments(), emptyList()) assertEquals(db.listIncomingPayments(5, 0), listOf(unexpiredPayment, paidPayment)) } @@ -1580,9 +1574,9 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val expected = ChannelCommand.Htlc.Settlement.Fulfill(add.id, preimage, commit = true) assertEquals(setOf(WrappedChannelCommand(add.channelId, expected)), result.actions.toSet()) - assertEquals(result.incomingPayment.received, result.received) - assertEquals(defaultAmount, result.received.amount) - assertEquals(listOf(IncomingPayment.ReceivedWith.LightningPayment(defaultAmount, add.channelId, 8, null)), result.received.receivedWith) + assertEqualsIgnoreTimestamps(result.incomingPayment.parts, result.parts) + assertEquals(defaultAmount, result.amount) + assertEqualsIgnoreTimestamps(listOf(LightningIncomingPayment.Part.Htlc(defaultAmount, add.channelId, 8, null)), result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } @@ -1605,7 +1599,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val add = makeUpdateAddHtlc(0, channelId, paymentHandler, paymentHash, finalPayload, route.firstPathKey) val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) - assertNull(result.incomingPayment.received) + assertTrue(result.incomingPayment.parts.isEmpty()) assertTrue(result.actions.isEmpty()) } @@ -1617,15 +1611,15 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val add = makeUpdateAddHtlc(1, channelId, paymentHandler, paymentHash, finalPayload, route.firstPathKey) val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) - val (expectedActions, expectedReceivedWith) = setOf( + val (expectedActions, parts) = setOf( // @formatter:off - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount1, channelId, 0, null), - WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount2, channelId, 1, null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(0, preimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount1, channelId, 0, null), + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(1, preimage, commit = true)) to LightningIncomingPayment.Part.Htlc(amount2, channelId, 1, null), // @formatter:on ).unzip() assertEquals(expectedActions.toSet(), result.actions.toSet()) - assertEquals(totalAmount, result.received.amount) - assertEquals(expectedReceivedWith, result.received.receivedWith) + assertEquals(totalAmount, result.amount) + assertEqualsIgnoreTimestamps(parts, result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } } @@ -1646,7 +1640,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(preimage, addLiquidity.preimage) assertEquals(defaultAmount, addLiquidity.paymentAmount) // We don't update the payments DB: we're waiting to receive HTLCs after the open/splice. - assertNull(paymentHandler.db.getIncomingPayment(paymentHash)?.received) + assertNull(paymentHandler.db.getLightningIncomingPayment(paymentHash)) } @Test @@ -1685,10 +1679,10 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertIs(result) val fulfill = ChannelCommand.Htlc.Settlement.Fulfill(add.id, preimage, commit = true) assertEquals(setOf(WrappedChannelCommand(add.channelId, fulfill)), result.actions.toSet()) - assertEquals(result.incomingPayment.received, result.received) - assertEquals(defaultAmount - payment.fundingFee.amount, result.received.amount) - val receivedWith = IncomingPayment.ReceivedWith.LightningPayment(defaultAmount - payment.fundingFee.amount, add.channelId, 0, payment.fundingFee) - assertEquals(listOf(receivedWith), result.received.receivedWith) + assertEqualsIgnoreTimestamps(result.incomingPayment.parts, result.parts) + assertEquals(defaultAmount - payment.fundingFee.amount, result.amount) + val parts = LightningIncomingPayment.Part.Htlc(defaultAmount - payment.fundingFee.amount, add.channelId, 0, payment.fundingFee) + assertEqualsIgnoreTimestamps(listOf(parts), result.parts) checkDbPayment(result.incomingPayment, paymentHandler.db) } } @@ -1697,7 +1691,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { fun `reject blinded payment for Bolt11 invoice`() = runSuspendTest { val (paymentHandler, incomingPayment, _) = createFixture(defaultAmount) val cltvExpiry = TestConstants.Bob.nodeParams.minFinalCltvExpiryDelta.toCltvExpiry(TestConstants.defaultBlockHeight.toLong()) - val (blindedPayload, route) = makeBlindedPayload(TestConstants.Bob.nodeParams.nodeId, defaultAmount, defaultAmount, cltvExpiry, preimage = incomingPayment.preimage) + val (blindedPayload, route) = makeBlindedPayload(TestConstants.Bob.nodeParams.nodeId, defaultAmount, defaultAmount, cltvExpiry, preimage = incomingPayment.paymentPreimage) val add = makeUpdateAddHtlc(8, randomBytes32(), paymentHandler, incomingPayment.paymentHash, blindedPayload, route.firstPathKey) val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) @@ -1725,7 +1719,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val add = makeUpdateAddHtlc(0, channelId, paymentHandler, paymentHash, finalPayload, route.firstPathKey) val result = paymentHandler.process(add, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, remoteFundingRates = null) assertIs(result) - assertNull(result.incomingPayment.received) + assertTrue(result.incomingPayment.parts.isEmpty()) assertTrue(result.actions.isEmpty()) } @@ -1780,6 +1774,16 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val defaultAmount = 150_000_000.msat val feeCreditFeatures = Features(Feature.ExperimentalSplice to FeatureSupport.Optional, Feature.OnTheFlyFunding to FeatureSupport.Optional, Feature.FundingFeeCredit to FeatureSupport.Optional) + fun LightningIncomingPayment.Part.resetTimestamp() = when (this) { + is LightningIncomingPayment.Part.Htlc -> copy(receivedAt = 0) + is LightningIncomingPayment.Part.FeeCredit -> copy(receivedAt = 0) + } + + fun List.resetTimestamp() = this.map { it.resetTimestamp() } + + fun assertEqualsIgnoreTimestamps(expected: List, actual: List) = + assertEquals(expected.resetTimestamp(), actual.resetTimestamp()) + private fun makeCmdAddHtlc(destination: PublicKey, paymentHash: ByteVector32, finalPayload: PaymentOnion.FinalPayload): ChannelCommand.Htlc.Add { val onion = OutgoingPaymentPacket.buildOnion(listOf(destination), listOf(finalPayload), paymentHash, OnionRoutingPacket.PaymentPacketLength).packet return ChannelCommand.Htlc.Add(finalPayload.amount, paymentHash, finalPayload.expiry, onion, UUID.randomUUID(), commit = true) @@ -1867,22 +1871,31 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { return Pair(payload, route) } - private suspend fun makeIncomingPayment(payee: IncomingPaymentHandler, amount: MilliSatoshi?, expiry: Duration? = null, timestamp: Long = currentTimestampSeconds()): Pair { + private suspend fun makeIncomingPayment(payee: IncomingPaymentHandler, amount: MilliSatoshi?, expiry: Duration? = null, timestamp: Long = currentTimestampSeconds()): Pair { val paymentRequest = payee.createInvoice(defaultPreimage, amount, Either.Left("unit test"), listOf(), expiry, timestamp) assertNotNull(paymentRequest.paymentMetadata) - return Pair(payee.db.getIncomingPayment(paymentRequest.paymentHash)!!, paymentRequest.paymentSecret) + return Pair(payee.db.getLightningIncomingPayment(paymentRequest.paymentHash)!!, paymentRequest.paymentSecret) } - private suspend fun checkDbPayment(incomingPayment: IncomingPayment, db: IncomingPaymentsDb) { - val dbPayment = db.getIncomingPayment(incomingPayment.paymentHash)!! - assertEquals(incomingPayment.preimage, dbPayment.preimage) + private suspend fun checkDbPayment(incomingPayment: LightningIncomingPayment, db: IncomingPaymentsDb) { + val dbPayment = db.getLightningIncomingPayment(incomingPayment.paymentHash)!! + assertEquals(incomingPayment.paymentPreimage, dbPayment.paymentPreimage) assertEquals(incomingPayment.paymentHash, dbPayment.paymentHash) - assertEquals(incomingPayment.origin, dbPayment.origin) + assertEquals( + when(incomingPayment) { + is Bolt11IncomingPayment -> incomingPayment.paymentRequest + is Bolt12IncomingPayment -> incomingPayment.metadata + }, + when(dbPayment) { + is Bolt11IncomingPayment -> dbPayment.paymentRequest + is Bolt12IncomingPayment -> dbPayment.metadata + } + ) assertEquals(incomingPayment.amount, dbPayment.amount) - assertEquals(incomingPayment.received?.receivedWith, dbPayment.received?.receivedWith) + assertEqualsIgnoreTimestamps(incomingPayment.parts, dbPayment.parts) } - private suspend fun createFixture(invoiceAmount: MilliSatoshi?): Triple { + private suspend fun createFixture(invoiceAmount: MilliSatoshi?): Triple { val paymentHandler = IncomingPaymentHandler(TestConstants.Bob.nodeParams, InMemoryPaymentsDb()) // We use a liquidity policy that accepts payment values used by default in this test file. paymentHandler.nodeParams.liquidityPolicy.emit(LiquidityPolicy.Auto(inboundLiquidityTarget = null, maxAbsoluteFee = 5_000.sat, maxRelativeFeeBasisPoints = 500, skipAbsoluteFeeCheck = false, maxAllowedFeeCredit = 0.msat)) @@ -1890,7 +1903,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { return Triple(paymentHandler, incomingPayment, paymentSecret) } - private suspend fun createFeeCreditFixture(invoiceAmount: MilliSatoshi, policy: LiquidityPolicy): Triple { + private suspend fun createFeeCreditFixture(invoiceAmount: MilliSatoshi, policy: LiquidityPolicy): Triple { val nodeParams = TestConstants.Bob.nodeParams.copy(features = TestConstants.Bob.nodeParams.features.add(Feature.FundingFeeCredit to FeatureSupport.Optional)) nodeParams.liquidityPolicy.emit(policy) val paymentHandler = IncomingPaymentHandler(nodeParams, InMemoryPaymentsDb())