diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt index e7bb9be6f..56d3a7ee9 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt @@ -88,7 +88,7 @@ sealed class ChannelAction { abstract val txId: TxId data class ViaSpliceOut(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId) : StoreOutgoingPayment() data class ViaSpliceCpfp(override val miningFees: Satoshi, override val txId: TxId) : StoreOutgoingPayment() - data class ViaInboundLiquidityRequest(override val txId: TxId, override val miningFees: Satoshi, val purchase: LiquidityAds.Purchase) : StoreOutgoingPayment() + data class ViaInboundLiquidityRequest(override val txId: TxId, val localMiningFees: Satoshi, val purchase: LiquidityAds.Purchase) : StoreOutgoingPayment() { override val miningFees: Satoshi = localMiningFees + purchase.fees.miningFee } data class ViaClose(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId, val isSentToDefaultAddress: Boolean, val closingType: ChannelClosingType) : StoreOutgoingPayment() } data class SetLocked(val txId: TxId) : Storage() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index e3c478ce6..c141be8f1 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -874,18 +874,22 @@ data class Normal( action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) } add(ChannelAction.Blockchain.SendWatch(watchConfirmed)) add(ChannelAction.Message.Send(action.localSigs)) - // If we received or sent funds as part of the splice, we will add a corresponding entry to our incoming/outgoing payments db - addAll(origins.map { origin -> - ChannelAction.Storage.StoreIncomingPayment.ViaSpliceIn( - amountReceived = origin.amountReceived(), - serviceFee = origin.fees.serviceFee.toMilliSatoshi(), - miningFee = origin.fees.miningFee, - localInputs = action.fundingTx.sharedTx.tx.localInputs.map { it.outPoint }.toSet(), - txId = action.fundingTx.txId, - origin = origin - ) - }) - // If we added some funds ourselves it's a swap-in + // If we purchased liquidity as part of the splice, we will add it to our payments db. + liquidityPurchase?.let { purchase -> + // If we are purchasing liquidity without any other operation (splice-in, splice-out or splice-cpfp), + // we must include the mining fees we're paying for the shared input and shared output. + // Otherwise, we only count the mining fees that we must refund to our peer as part of the liquidity + // purchase: the mining fees we pay for our inputs/outputs and the shared input/output will be recorded + // in the dedicated splice entry below. + val isPurchaseOnly = action.fundingTx.sharedTx.tx.let { + action.fundingTx.fundingParams.isInitiator && it.localInputs.isEmpty() && it.localOutputs.isEmpty() && it.remoteInputs.isNotEmpty() + } + val localMiningFees = if (isPurchaseOnly) action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi() else 0.sat + add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, localMiningFees = localMiningFees, purchase = purchase)) + add(ChannelAction.EmitEvent(LiquidityEvents.Purchased(purchase))) + } + // NB: the following assumes that there can't be a splice-in and a splice-out simultaneously, + // or more than one splice-out, because we attribute all local mining fees to each payment entry. 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, @@ -893,7 +897,7 @@ data class Normal( miningFee = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), localInputs = action.fundingTx.sharedTx.tx.localInputs.map { it.outPoint }.toSet(), txId = action.fundingTx.txId, - origin = null + origin = origins.filterIsInstance().firstOrNull() ) ) addAll(action.fundingTx.fundingParams.localOutputs.map { txOut -> @@ -904,17 +908,10 @@ data class Normal( txId = action.fundingTx.txId ) }) - // If we initiated the splice but there are no new inputs on either side and no new output on our side, it's a cpfp + // If we initiated the splice but there are no new inputs on either side and no new output on our side, it's a cpfp. if (action.fundingTx.fundingParams.isInitiator && action.fundingTx.sharedTx.tx.localInputs.isEmpty() && action.fundingTx.sharedTx.tx.remoteInputs.isEmpty() && action.fundingTx.fundingParams.localOutputs.isEmpty()) { add(ChannelAction.Storage.StoreOutgoingPayment.ViaSpliceCpfp(miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), txId = action.fundingTx.txId)) } - liquidityPurchase?.let { purchase -> - // The actual mining fees contain the inputs and outputs we paid for in the interactive-tx transaction, - // and what we refunded the remote peer for some of their inputs and outputs via the lease. - val miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi() + purchase.fees.miningFee - add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, miningFees = miningFees, purchase = purchase)) - add(ChannelAction.EmitEvent(LiquidityEvents.Purchased(purchase))) - } origins.filterIsInstance().forEach { origin -> add(ChannelAction.EmitEvent(SwapInEvents.Accepted(origin.inputs, origin.amountBeforeFees.truncateToSatoshi(), origin.fees))) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt index ef90a7b6c..8857c36b0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt @@ -10,7 +10,7 @@ import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.channel.* import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.utils.msat -import fr.acinq.lightning.utils.toMilliSatoshi +import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.* import kotlin.math.absoluteValue @@ -119,26 +119,29 @@ data class WaitForFundingSigned( action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) } add(ChannelAction.Blockchain.SendWatch(watchConfirmed)) add(ChannelAction.Message.Send(action.localSigs)) - // If we receive funds as part of the channel creation, we will add it to our payments db + // If we purchased liquidity as part of the channel creation, we will add it to our payments db. + liquidityPurchase?.let { purchase -> + if (channelParams.localParams.isChannelOpener) { + // We only count the mining fees that we must refund to our peer as part of the liquidity purchase. + // If we're also contributing to the funding transaction, the mining fees we pay for our inputs and + // outputs will be recorded in the ViaNewChannel incoming payment entry below. + add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, localMiningFees = 0.sat, purchase = purchase)) + add(ChannelAction.EmitEvent(LiquidityEvents.Purchased(purchase))) + } + } + // If we receive funds as part of the channel creation, we will add it to our payments db. if (action.commitment.localCommit.spec.toLocal > 0.msat) add( ChannelAction.Storage.StoreIncomingPayment.ViaNewChannel( amountReceived = action.commitment.localCommit.spec.toLocal, - serviceFee = channelOrigin?.fees?.serviceFee?.toMilliSatoshi() ?: 0.msat, - miningFee = channelOrigin?.fees?.miningFee ?: action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), + serviceFee = 0.msat, + // We only count the mining fees we're paying for our inputs and outputs. + // The mining fees for the remote inputs and outputs are paid by the remote node. + miningFee = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), localInputs = action.fundingTx.sharedTx.tx.localInputs.map { it.outPoint }.toSet(), txId = action.fundingTx.txId, origin = channelOrigin ) ) - liquidityPurchase?.let { purchase -> - if (channelParams.localParams.isChannelOpener) { - // The actual mining fees contain the inputs and outputs we paid for in the interactive-tx transaction, - // and what we refunded the remote peer for some of their inputs and outputs via the lease. - val miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi() + purchase.fees.miningFee - add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, miningFees = miningFees, purchase = purchase)) - add(ChannelAction.EmitEvent(LiquidityEvents.Purchased(purchase))) - } - } listOfNotNull(channelOrigin).filterIsInstance().forEach { origin -> add(ChannelAction.EmitEvent(SwapInEvents.Accepted(origin.inputs, origin.amountBeforeFees.truncateToSatoshi(), origin.fees))) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt index 9456c2767..09bc12e4e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt @@ -177,7 +177,7 @@ data class IncomingPayment(val preimage: ByteVector32, val origin: Origin, val r } /** - * Payment was added to our fee credit for future on-chain operations (see [Feature.FundingFeeCredit]). + * 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() { @@ -427,14 +427,15 @@ data class InboundLiquidityOutgoingPayment( override val id: UUID, override val channelId: ByteVector32, override val txId: TxId, - override val miningFees: Satoshi, + val localMiningFees: Satoshi, val purchase: LiquidityAds.Purchase, override val createdAt: Long, override val confirmedAt: Long?, override val lockedAt: Long?, ) : OnChainOutgoingPayment() { + override val miningFees: Satoshi = localMiningFees + purchase.fees.miningFee val serviceFees: Satoshi = purchase.fees.serviceFee - override val fees: MilliSatoshi = (miningFees + serviceFees).toMilliSatoshi() + override val fees: MilliSatoshi = (localMiningFees + purchase.fees.total).toMilliSatoshi() override val amount: MilliSatoshi = fees override val completedAt: Long? = lockedAt val fundingFee: LiquidityAds.FundingFee = LiquidityAds.FundingFee(purchase.fees.total.toMilliSatoshi(), txId) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index a24a9d5cb..9f1f6d904 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -887,7 +887,7 @@ class Peer( id = UUID.randomUUID(), channelId = channelId, txId = action.txId, - miningFees = action.miningFees, + localMiningFees = action.localMiningFees, purchase = action.purchase, createdAt = currentTimestampMillis(), confirmedAt = null, diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt index a3aa4855a..dd2152f6b 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt @@ -863,7 +863,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { splice.fees(TestConstants.feeratePerKw, isChannelCreation = false), LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(incomingPayment.paymentHash)), ) - val payment = InboundLiquidityOutgoingPayment(UUID.randomUUID(), channelId, TxId(randomBytes32()), 500.sat, purchase, 0, null, null) + val payment = InboundLiquidityOutgoingPayment(UUID.randomUUID(), channelId, TxId(randomBytes32()), 0.sat, purchase, 0, null, null) paymentHandler.db.addOutgoingPayment(payment) payment } @@ -963,7 +963,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { LiquidityAds.Fees(2000.sat, 3000.sat), LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(incomingPayment.paymentHash)), ) - val payment = InboundLiquidityOutgoingPayment(UUID.randomUUID(), channelId, TxId(randomBytes32()), 500.sat, purchase, 0, null, null) + val payment = InboundLiquidityOutgoingPayment(UUID.randomUUID(), channelId, TxId(randomBytes32()), 100.sat, purchase, 0, null, null) paymentHandler.db.addOutgoingPayment(payment) run { @@ -999,7 +999,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { LiquidityAds.Fees(2000.sat, 3000.sat), LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(listOf(incomingPayment.paymentHash)), ) - val payment = InboundLiquidityOutgoingPayment(UUID.randomUUID(), channelId, TxId(randomBytes32()), 500.sat, purchase, 0, null, null) + val payment = InboundLiquidityOutgoingPayment(UUID.randomUUID(), channelId, TxId(randomBytes32()), 0.sat, purchase, 0, null, null) paymentHandler.db.addOutgoingPayment(payment) val add = makeUpdateAddHtlc(0, channelId, paymentHandler, incomingPayment.paymentHash, makeMppPayload(defaultAmount, defaultAmount, paymentSecret), fundingFee = payment.fundingFee) @@ -1022,7 +1022,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { 250_000.msat, LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(randomBytes32())), ) - val payment = InboundLiquidityOutgoingPayment(UUID.randomUUID(), channelId, TxId(randomBytes32()), 500.sat, purchase, 0, null, null) + val payment = InboundLiquidityOutgoingPayment(UUID.randomUUID(), channelId, TxId(randomBytes32()), 0.sat, purchase, 0, null, null) paymentHandler.db.addOutgoingPayment(payment) val add = makeUpdateAddHtlc(0, channelId, paymentHandler, incomingPayment.paymentHash, makeMppPayload(defaultAmount, defaultAmount, paymentSecret), fundingFee = payment.fundingFee)