From 2a69fe318825017c867953321d34a7b6c22eb9f2 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 13 Sep 2024 17:30:39 +0200 Subject: [PATCH 01/13] initial integration --- buildSrc/src/main/kotlin/Versions.kt | 2 +- .../kotlin/fr/acinq/lightning/bin/Api.kt | 12 +++-- .../kotlin/fr/acinq/lightning/bin/Main.kt | 54 +++++++++---------- .../kotlin/fr/acinq/lightning/bin/conf/Lsp.kt | 35 +----------- .../db/payments/InboundLiquidityLeaseType.kt | 2 +- .../lightning/bin/json/JsonSerializers.kt | 2 +- 6 files changed, 39 insertions(+), 68 deletions(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 94ea9bf..c5a060e 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,6 +1,6 @@ object Versions { val kotlin = "1.9.23" - val lightningKmp = "1.7.3-FEECREDIT-11" + val lightningKmp = "1.7.4-SNAPSHOT" val sqlDelight = "2.0.1" val okio = "3.8.0" val clikt = "4.2.2" diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt index e6ba690..49f2561 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt @@ -162,13 +162,19 @@ class Api( .filterNot { it is Closing || it is Closed } .map { it.commitments.active.first().availableBalanceForSend(it.commitments.params, it.commitments.changes) } .sum().truncateToSatoshi() - call.respond(Balance(balance, nodeParams.feeCredit.value)) + call.respond(Balance(balance, peer.feeCreditFlow.value.truncateToSatoshi())) } get("estimateliquidityfees") { val amount = call.parameters.getLong("amountSat").sat val feerate = peer.onChainFeeratesFlow.filterNotNull().first().fundingFeerate - val liquidityFees = LSP.liquidityFees(amount, feerate, isNew = peer.channels.isEmpty()) - call.respond(LiquidityFees(liquidityFees)) + val fundingRates = peer.remoteFundingRates.filterNotNull().first() + when (val fundingRate = fundingRates.findRate(amount)) { + null -> badRequest("no available funding rates for amount=$amount") + else -> { + val liquidityFees = fundingRate.fees(feerate, amount, amount, isChannelCreation = peer.channels.isEmpty()) + call.respond(LiquidityFees(liquidityFees)) + } + } } get("listchannels") { call.respond(peer.channels.values.toList()) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt index 11d6ae8..0f78670 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt @@ -47,10 +47,7 @@ import fr.acinq.lightning.io.Peer import fr.acinq.lightning.io.TcpSocket import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.payment.LiquidityPolicy -import fr.acinq.lightning.utils.Connection -import fr.acinq.lightning.utils.msat -import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.utils.toByteVector +import fr.acinq.lightning.utils.* import fr.acinq.phoenix.db.* import io.ktor.http.* import io.ktor.server.application.* @@ -90,14 +87,14 @@ class Phoenixd : CliktCommand() { } private val agreeToTermsOfService by option("--agree-to-terms-of-service", hidden = true, help = "Agree to terms of service").flag() private val chain by option("--chain", help = "Bitcoin chain to use").choice( - "mainnet" to Chain.Mainnet, "testnet" to Chain.Testnet + "mainnet" to Chain.Mainnet, "testnet" to Chain.Testnet3 ).default(Chain.Mainnet, defaultForHelp = "mainnet") private val mempoolSpaceUrl by option("--mempool-space-url", help = "Custom mempool.space instance") .convert { Url(it) } .defaultLazy { when (chain) { Chain.Mainnet -> MempoolSpaceClient.OfficialMempoolMainnet - Chain.Testnet -> MempoolSpaceClient.OfficialMempoolTestnet + Chain.Testnet3 -> MempoolSpaceClient.OfficialMempoolTestnet else -> error("unsupported chain") } } @@ -155,7 +152,7 @@ class Phoenixd : CliktCommand() { "off" to 0.sat, "50k" to 50_000.sat, "100k" to 100_000.sat, - ).default(100_000.sat, "100k") + ).convert { it.toMilliSatoshi() }.default(100_000.sat.toMilliSatoshi(), "100k") private val maxRelativeFeePct by option("--max-relative-fee-percent", help = "Max relative fee for on-chain operations in percent.", hidden = true) .int() .restrictTo(1..50) @@ -244,10 +241,11 @@ class Phoenixd : CliktCommand() { ) val lsp = LSP.from(chain) val liquidityPolicy = LiquidityPolicy.Auto( - maxMiningFee = liquidityOptions.maxMiningFee, + inboundLiquidityTarget = liquidityOptions.autoLiquidity, + maxAbsoluteFee = liquidityOptions.maxMiningFee, maxRelativeFeeBasisPoints = liquidityOptions.maxRelativeFeeBasisPoints, - skipMiningFeeCheck = false, - maxAllowedCredit = liquidityOptions.maxFeeCredit + skipAbsoluteFeeCheck = false, + maxAllowedFeeCredit = liquidityOptions.maxFeeCredit ) val keyManager = LocalKeyManager(seed.seed, chain, lsp.swapInXpub) val nodeParams = NodeParams(chain, loggerFactory, keyManager) @@ -332,30 +330,32 @@ class Phoenixd : CliktCommand() { } launch { nodeParams.nodeEvents - .filterIsInstance() + .filterIsInstance() .collect { when (val reason = it.reason) { - is LiquidityEvents.Decision.Rejected.Reason.OverMaxCredit -> { - consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over max fee credit (max=${reason.maxAllowedCredit})")) - } - is LiquidityEvents.Decision.Rejected.Reason.TooExpensive.OverMaxMiningFee -> { - consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over max mining fee (max=${reason.maxMiningFee})")) - } - is LiquidityEvents.Decision.Rejected.Reason.TooExpensive.OverRelativeFee -> { +// is LiquidityEvents.Rejected.Reason.OverMaxCredit -> { +// consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over max fee credit (max=${reason.maxAllowedCredit})")) +// } + is LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee -> + consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over max mining fee (max=${reason.maxAbsoluteFee})")) + is LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee -> consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): fee=${it.fee.truncateToSatoshi()} more than ${reason.maxRelativeFeeBasisPoints.toDouble() / 100}% of amount")) - } - LiquidityEvents.Decision.Rejected.Reason.ChannelInitializing -> { - consoleLog(yellow("channels are initializing")) - } - LiquidityEvents.Decision.Rejected.Reason.PolicySetToDisabled -> { + LiquidityEvents.Rejected.Reason.PolicySetToDisabled -> consoleLog(yellow("automated liquidity is disabled")) - } + LiquidityEvents.Rejected.Reason.ChannelFundingInProgress -> + consoleLog(yellow("channel operation is in progress")) + is LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow -> + consoleLog(yellow("missing offchain amount is too low (missingOffChainAmount=${reason.missingOffChainAmount} currentFeeCredit=${reason.currentFeeCredit}")) + LiquidityEvents.Rejected.Reason.NoMatchingFundingRate -> + consoleLog(yellow("no matching funding rates")) + is LiquidityEvents.Rejected.Reason.TooManyParts -> + consoleLog(yellow("too many payment parts")) } } } launch { - nodeParams.feeCredit - .drop(1) // we drop the initial value which is 0 sat + peer.feeCreditFlow + .drop(1) // we drop the initial value which is 0 msat .collect { feeCredit -> consoleLog("fee credit: $feeCredit") } } } @@ -370,8 +370,6 @@ class Phoenixd : CliktCommand() { runBlocking { peer.connectionState.first { it == Connection.ESTABLISHED } - peer.registerFcmToken("super-${randomBytes32().toHex()}") - peer.setAutoLiquidityParams(liquidityOptions.autoLiquidity) } val server = embeddedServer(CIO, port = httpBindPort, host = httpBindIp, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt index 4c43db7..a55f417 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt @@ -2,12 +2,9 @@ package fr.acinq.lightning.bin.conf import fr.acinq.bitcoin.Chain import fr.acinq.bitcoin.PublicKey -import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.* -import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.wire.LiquidityAds data class LSP(val walletParams: WalletParams, val swapInXpub: String) { @@ -44,7 +41,7 @@ data class LSP(val walletParams: WalletParams, val swapInXpub: String) { swapInParams ) ) - is Chain.Testnet -> LSP( + is Chain.Testnet3 -> LSP( swapInXpub = "tpubDAmCFB21J9ExKBRPDcVxSvGs9jtcf8U1wWWbS1xTYmnUsuUHPCoFdCnEGxLE3THSWcQE48GHJnyz8XPbYUivBMbLSMBifFd3G9KmafkM9og", walletParams = WalletParams( trampolineNode = NodeUri(PublicKey.fromHex("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134"), "13.248.222.197", 9735), @@ -55,35 +52,5 @@ data class LSP(val walletParams: WalletParams, val swapInXpub: String) { ) else -> error("unsupported chain $chain") } - - fun liquidityFees(amount: Satoshi, feerate: FeeratePerKw, isNew: Boolean): LiquidityAds.LeaseFees { - val creationFee = if (isNew) 1_000.sat else 0.sat - val leaseRate = liquidityLeaseRate(amount) - val leaseFees = leaseRate.fees(feerate, requestedAmount = amount, contributedAmount = amount) - return leaseFees.copy(serviceFee = creationFee + leaseFees.serviceFee) - } - - private fun liquidityLeaseRate(amount: Satoshi): LiquidityAds.LeaseRate { - // WARNING : THIS MUST BE KEPT IN SYNC WITH LSP OTHERWISE FUNDING REQUEST WILL BE REJECTED BY PHOENIX - val fundingWeight = if (amount <= 100_000.sat) { - 271 * 2 // 2-inputs (wpkh) / 0-change - } else if (amount <= 250_000.sat) { - 271 * 2 // 2-inputs (wpkh) / 0-change - } else if (amount <= 500_000.sat) { - 271 * 4 // 4-inputs (wpkh) / 0-change - } else if (amount <= 1_000_000.sat) { - 271 * 4 // 4-inputs (wpkh) / 0-change - } else { - 271 * 6 // 6-inputs (wpkh) / 0-change - } - return LiquidityAds.LeaseRate( - leaseDuration = 0, - fundingWeight = fundingWeight, - leaseFeeProportional = 100, // 1% - leaseFeeBase = 0.sat, - maxRelayFeeProportional = 100, - maxRelayFeeBase = 1_000.msat - ) - } } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityLeaseType.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityLeaseType.kt index 2801eb9..e674ff6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityLeaseType.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityLeaseType.kt @@ -66,7 +66,7 @@ sealed class InboundLiquidityLeaseData { fun deserialize( typeVersion: InboundLiquidityLeaseTypeVersion, blob: ByteArray, - ): LiquidityAds.Lease = DbTypesHelper.decodeBlob(blob) { json, format -> + ): LiquidityAds.Purchase = DbTypesHelper.decodeBlob(blob) { json, format -> when (typeVersion) { InboundLiquidityLeaseTypeVersion.LEASE_V0 -> format.decodeFromString(json).let { LiquidityAds.Lease( diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt index 22819c2..aa15290 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt @@ -77,7 +77,7 @@ sealed class ApiType { @Serializable data class LiquidityFees(@SerialName("miningFeeSat") val miningFee: Satoshi, @SerialName("serviceFeeSat") val serviceFee: Satoshi) : ApiType() { - constructor(leaseFees: LiquidityAds.LeaseFees) : this(leaseFees.miningFee, leaseFees.serviceFee) + constructor(leaseFees: LiquidityAds.Fees) : this(leaseFees.miningFee, leaseFees.serviceFee) } @Serializable From 6cc085c108426e9bc2cd0f2df26ea14078409885 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:50:53 +0200 Subject: [PATCH 02/13] Update database for new liquidity ads purchase type Note that we are using a polymorphic serialiser to store the liquidity purchase data in the database (data type is stored in the blob). A new received-with db mapping has been added to handle the optional funding fee type for HTLC payments. Also fixed the database file name when creating the db drivers, due to the testnet3/4 changes. --- .../kotlin/fr/acinq/lightning/bin/Main.kt | 5 +- .../lightning/bin/db/SqlitePaymentsDb.kt | 6 + .../bin/db/payments/DbTypesHelper.kt | 12 + .../db/payments/InboundLiquidityLeaseType.kt | 103 --------- .../payments/InboundLiquidityPurchaseType.kt | 213 ++++++++++++++++++ .../db/payments/InboundLiquidityQueries.kt | 18 +- .../db/payments/IncomingReceivedWithType.kt | 49 ++-- .../db/serializers/v1/FundingFeeSerializer.kt | 53 +++++ .../db/InboundLiquidityOutgoingPayments.sq | 23 +- .../sqldelight/phoenixdb/migrations/2.sqm | 12 + .../kotlin/fr/acinq/lightning/bin/Actuals.kt | 6 +- .../kotlin/fr/acinq/lightning/bin/Actuals.kt | 6 +- 12 files changed, 371 insertions(+), 135 deletions(-) delete mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityLeaseType.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityPurchaseType.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/FundingFeeSerializer.kt create mode 100644 src/commonMain/sqldelight/phoenixdb/migrations/2.sqm diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt index 0f78670..97f91ca 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt @@ -275,8 +275,9 @@ class Phoenixd : CliktCommand() { closing_info_typeAdapter = EnumColumnAdapter() ), inbound_liquidity_outgoing_paymentsAdapter = Inbound_liquidity_outgoing_payments.Adapter( - lease_typeAdapter = EnumColumnAdapter() - ) + lease_typeAdapter = EnumColumnAdapter(), + payment_details_typeAdapter = EnumColumnAdapter() + ), ) val channelsDb = SqliteChannelsDb(driver, database) val paymentsDb = SqlitePaymentsDb(database) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt index 18ad171..e081a16 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt @@ -98,6 +98,12 @@ class SqlitePaymentsDb(val database: PhoenixDatabase) : PaymentsDb { } } + override suspend fun getInboundLiquidityPurchase(fundingTxId: TxId): InboundLiquidityOutgoingPayment? { + return withContext(Dispatchers.Default) { + inboundLiquidityQueries.getByTxId(fundingTxId) + } + } + override suspend fun completeOutgoingPaymentOffchain( id: UUID, finalFailure: FinalFailure, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/DbTypesHelper.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/DbTypesHelper.kt index 3947f46..316b790 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/DbTypesHelper.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/DbTypesHelper.kt @@ -29,11 +29,23 @@ object DbTypesHelper { val module = SerializersModule { polymorphic(IncomingReceivedWithData.Part::class) { + @Suppress("DEPRECATION") subclass(IncomingReceivedWithData.Part.Htlc.V0::class) + subclass(IncomingReceivedWithData.Part.Htlc.V1::class) subclass(IncomingReceivedWithData.Part.NewChannel.V2::class) subclass(IncomingReceivedWithData.Part.SpliceIn.V0::class) subclass(IncomingReceivedWithData.Part.FeeCredit.V0::class) } + polymorphic(InboundLiquidityPaymentDetailsData::class) { + subclass(InboundLiquidityPaymentDetailsData.ChannelBalance.V0::class) + subclass(InboundLiquidityPaymentDetailsData.FutureHtlc.V0::class) + subclass(InboundLiquidityPaymentDetailsData.FutureHtlcWithPreimage.V0::class) + subclass(InboundLiquidityPaymentDetailsData.ChannelBalanceForFutureHtlc.V0::class) + } + polymorphic(InboundLiquidityPurchaseData::class) { + subclass(InboundLiquidityPurchaseData.Standard.V0::class) + subclass(InboundLiquidityPurchaseData.WithFeeCredit.V0::class) + } } val polymorphicFormat = Json { serializersModule = module } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityLeaseType.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityLeaseType.kt deleted file mode 100644 index e674ff6..0000000 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityLeaseType.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2023 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:UseSerializers( - ByteVectorSerializer::class, - ByteVector32Serializer::class, - ByteVector64Serializer::class, - SatoshiSerializer::class, - MilliSatoshiSerializer::class -) - -package fr.acinq.lightning.bin.db.payments - -import fr.acinq.bitcoin.ByteVector -import fr.acinq.bitcoin.ByteVector64 -import fr.acinq.bitcoin.Satoshi -import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment -import fr.acinq.lightning.wire.LiquidityAds -import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer -import fr.acinq.lightning.bin.db.serializers.v1.ByteVector64Serializer -import fr.acinq.lightning.bin.db.serializers.v1.ByteVectorSerializer -import fr.acinq.lightning.bin.db.serializers.v1.MilliSatoshiSerializer -import fr.acinq.lightning.bin.db.serializers.v1.SatoshiSerializer -import io.ktor.utils.io.charsets.Charsets -import io.ktor.utils.io.core.toByteArray -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -enum class InboundLiquidityLeaseTypeVersion { - LEASE_V0, -} - -sealed class InboundLiquidityLeaseData { - - @Serializable - data class V0( - val amount: Satoshi, - val miningFees: Satoshi, - val serviceFee: Satoshi, - val sellerSig: ByteVector64, - val witnessFundingScript: ByteVector, - val witnessLeaseDuration: Int, - val witnessLeaseEnd: Int, - val witnessMaxRelayFeeProportional: Int, - val witnessMaxRelayFeeBase: MilliSatoshi - ) : InboundLiquidityLeaseData() - - companion object { - /** Deserializes a json-encoded blob containing data for an [LiquidityAds.Lease] object. */ - fun deserialize( - typeVersion: InboundLiquidityLeaseTypeVersion, - blob: ByteArray, - ): LiquidityAds.Purchase = DbTypesHelper.decodeBlob(blob) { json, format -> - when (typeVersion) { - InboundLiquidityLeaseTypeVersion.LEASE_V0 -> format.decodeFromString(json).let { - LiquidityAds.Lease( - amount = it.amount, - fees = LiquidityAds.LeaseFees(miningFee = it.miningFees, serviceFee = it.serviceFee), - sellerSig = it.sellerSig, - witness = LiquidityAds.LeaseWitness( - fundingScript = it.witnessFundingScript, - leaseDuration = it.witnessLeaseDuration, - leaseEnd = it.witnessLeaseEnd, - maxRelayFeeProportional = it.witnessMaxRelayFeeProportional, - maxRelayFeeBase = it.witnessMaxRelayFeeBase, - ) - ) - } - } - } - } -} - -fun InboundLiquidityOutgoingPayment.mapLeaseToDb() = InboundLiquidityLeaseTypeVersion.LEASE_V0 to - InboundLiquidityLeaseData.V0( - amount = lease.amount, - miningFees = lease.fees.miningFee, - serviceFee = lease.fees.serviceFee, - sellerSig = lease.sellerSig, - witnessFundingScript = lease.witness.fundingScript, - witnessLeaseDuration = lease.witness.leaseDuration, - witnessLeaseEnd = lease.witness.leaseEnd, - witnessMaxRelayFeeProportional = lease.witness.maxRelayFeeProportional, - witnessMaxRelayFeeBase = lease.witness.maxRelayFeeBase, - ).let { - Json.encodeToString(it).toByteArray(Charsets.UTF_8) - } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityPurchaseType.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityPurchaseType.kt new file mode 100644 index 0000000..d85e64d --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityPurchaseType.kt @@ -0,0 +1,213 @@ +/* + * Copyright 2023 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:UseSerializers( + ByteVectorSerializer::class, + ByteVector32Serializer::class, + ByteVector64Serializer::class, + SatoshiSerializer::class, + MilliSatoshiSerializer::class +) + +package fr.acinq.lightning.bin.db.payments + +import fr.acinq.bitcoin.ByteVector +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.ByteVector64 +import fr.acinq.bitcoin.Satoshi +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.bin.db.serializers.v1.* +import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment +import fr.acinq.lightning.wire.LiquidityAds +import io.ktor.utils.io.charsets.* +import io.ktor.utils.io.core.* +import kotlinx.serialization.PolymorphicSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers + +enum class InboundLiquidityPurchaseType { + @Deprecated("obsolete with the new on-the-fly channel funding that replaces lease -> purchase") + LEASE_V0, + PURCHASE_STANDARD, + PURCHASE_FEE_CREDIT, +} + +enum class InboundLiquidityPaymentDetailsType { + CHANNEL_BALANCE, + FUTURE_HTLC, + FUTURE_HTLC_WITH_PREIMAGE, + CHANNEL_BALANCE_FUTURE_HTLC, +} + +@Suppress("DEPRECATION") +@Deprecated("obsolete with the new on-the-fly channel funding that replaces lease -> purchase") +sealed class InboundLiquidityLeaseData { + + @Serializable + data class V0( + // these legacy data can still be mapped to the new model + val amount: Satoshi, + val miningFees: Satoshi, + val serviceFee: Satoshi, + // the other legacy data are unused and ignored + val sellerSig: ByteVector64, + val witnessFundingScript: ByteVector, + val witnessLeaseDuration: Int, + val witnessLeaseEnd: Int, + val witnessMaxRelayFeeProportional: Int, + val witnessMaxRelayFeeBase: MilliSatoshi + ) : InboundLiquidityLeaseData() +} + +@Serializable +sealed class InboundLiquidityPaymentDetailsData { + sealed class ChannelBalance : InboundLiquidityPaymentDetailsData() { + @Serializable + data object V0 : ChannelBalance() + } + + sealed class FutureHtlc : InboundLiquidityPaymentDetailsData() { + @Serializable + data class V0(val paymentHashes: List) : FutureHtlc() + } + + sealed class FutureHtlcWithPreimage : InboundLiquidityPaymentDetailsData() { + @Serializable + data class V0(val preimages: List) : FutureHtlcWithPreimage() + } + + sealed class ChannelBalanceForFutureHtlc : InboundLiquidityPaymentDetailsData() { + @Serializable + data class V0(val paymentHashes: List) : ChannelBalanceForFutureHtlc() + } + + companion object { + /** Deserializes a json-encoded blob containing a [LiquidityAds.PaymentDetails] object. */ + fun deserialize(blob: ByteArray): LiquidityAds.PaymentDetails = DbTypesHelper.decodeBlob(blob) { json, _ -> + when (val data = DbTypesHelper.polymorphicFormat.decodeFromString(PolymorphicSerializer(InboundLiquidityPaymentDetailsData::class), json)) { + is ChannelBalance.V0 -> LiquidityAds.PaymentDetails.FromChannelBalance + is FutureHtlc.V0 -> LiquidityAds.PaymentDetails.FromFutureHtlc(data.paymentHashes) + is FutureHtlcWithPreimage.V0 -> LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage(data.preimages) + is ChannelBalanceForFutureHtlc.V0 -> LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(data.paymentHashes) + } + } + } +} + +@Serializable +sealed class InboundLiquidityPurchaseData { + + sealed class Standard : InboundLiquidityPurchaseData() { + @Serializable + data class V0( + val amount: Satoshi, + val miningFees: Satoshi, + val serviceFee: Satoshi, + ) : Standard() + } + + sealed class WithFeeCredit : InboundLiquidityPurchaseData() { + @Serializable + data class V0( + val amount: Satoshi, + val miningFees: Satoshi, + val serviceFee: Satoshi, + val feeCreditUsed: MilliSatoshi, + ) : WithFeeCredit() + } + + companion object { + /** + * Deserializes purchase and payment_details json-encoded blobs into a [LiquidityAds.Purchase] object. + * + * @param typeVersion only used for the legacy leased data, where the blob did not contain the type of the object. The "modern" blobs are expected + * to have been created by a polymorphic serializer, hence they already contain the type and do not need the type_version. + * */ + @Suppress("DEPRECATION") + fun deserialize( + typeVersion: InboundLiquidityPurchaseType, + blob: ByteArray, + paymentDetailsBlob: ByteArray, + ): LiquidityAds.Purchase = DbTypesHelper.decodeBlob(blob) { json, format -> + when (typeVersion) { + // map legacy lease data into the modern [LiquidityAds.Purchase] object ; uses fake payment details data + InboundLiquidityPurchaseType.LEASE_V0 -> format.decodeFromString(json).let { + LiquidityAds.Purchase.Standard( + amount = it.amount, + fees = LiquidityAds.Fees(miningFee = it.miningFees, serviceFee = it.serviceFee), + paymentDetails = LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(ByteVector32.Zeroes)) + ) + } + else -> { + when (val data = DbTypesHelper.polymorphicFormat.decodeFromString(PolymorphicSerializer(InboundLiquidityPurchaseData::class), json)) { + is Standard.V0 -> LiquidityAds.Purchase.Standard( + amount = data.amount, + fees = LiquidityAds.Fees(miningFee = data.miningFees, serviceFee = data.serviceFee), + paymentDetails = InboundLiquidityPaymentDetailsData.deserialize(paymentDetailsBlob) + ) + is WithFeeCredit.V0 -> LiquidityAds.Purchase.WithFeeCredit( + amount = data.amount, + fees = LiquidityAds.Fees(miningFee = data.miningFees, serviceFee = data.serviceFee), + feeCreditUsed = data.feeCreditUsed, + paymentDetails = InboundLiquidityPaymentDetailsData.deserialize(paymentDetailsBlob) + ) + } + } + } + } + } +} + +/** + * Maps a [LiquidityAds.Purchase] object into 2 pairs containing the purchase type+json and the payment details type+json. + * + * Note that payment details are mapped to a separate object because we want to store/view these payment details information + * in a separate column in the database (and not inside the purchase blob). + */ +fun InboundLiquidityOutgoingPayment.mapPurchaseToDb(): Pair, + Pair> { + + // map a [LiquidityAds.PaymentDetails] object into the relevant type/data pair. */ + val (detailsType, detailsData) = when (val d = purchase.paymentDetails) { + is LiquidityAds.PaymentDetails.FromChannelBalance -> InboundLiquidityPaymentDetailsType.CHANNEL_BALANCE to InboundLiquidityPaymentDetailsData.ChannelBalance.V0 + is LiquidityAds.PaymentDetails.FromFutureHtlc -> InboundLiquidityPaymentDetailsType.FUTURE_HTLC to InboundLiquidityPaymentDetailsData.FutureHtlc.V0(d.paymentHashes) + is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> InboundLiquidityPaymentDetailsType.FUTURE_HTLC_WITH_PREIMAGE to InboundLiquidityPaymentDetailsData.FutureHtlcWithPreimage.V0(d.preimages) + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> InboundLiquidityPaymentDetailsType.CHANNEL_BALANCE_FUTURE_HTLC to InboundLiquidityPaymentDetailsData.ChannelBalanceForFutureHtlc.V0(d.paymentHashes) + } + + // map a [LiquidityAds.Purchase] object into the relevant type/data pair. + val (purchaseType, purchaseData) = when (val p = this.purchase) { + is LiquidityAds.Purchase.Standard -> InboundLiquidityPurchaseType.PURCHASE_STANDARD to InboundLiquidityPurchaseData.Standard.V0( + amount = purchase.amount, + miningFees = purchase.fees.miningFee, + serviceFee = purchase.fees.serviceFee, + ) + + is LiquidityAds.Purchase.WithFeeCredit -> InboundLiquidityPurchaseType.PURCHASE_FEE_CREDIT to InboundLiquidityPurchaseData.WithFeeCredit.V0( + amount = purchase.amount, + miningFees = purchase.fees.miningFee, + serviceFee = purchase.fees.serviceFee, + feeCreditUsed = p.feeCreditUsed, + ) + } + + // encode data with a polymorphic serializer + return (purchaseType to purchaseData.let { + DbTypesHelper.polymorphicFormat.encodeToString(PolymorphicSerializer(InboundLiquidityPurchaseData::class), it).toByteArray(Charsets.UTF_8) + }) to (detailsType to detailsData.let { + DbTypesHelper.polymorphicFormat.encodeToString(PolymorphicSerializer(InboundLiquidityPaymentDetailsData::class), it).toByteArray(Charsets.UTF_8) + }) +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt index 3600e7e..7ebf768 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt @@ -28,14 +28,16 @@ class InboundLiquidityQueries(val database: PhoenixDatabase) { fun add(payment: InboundLiquidityOutgoingPayment) { database.transaction { - val (leaseType, leaseData) = payment.mapLeaseToDb() + val (purchase, details) = payment.mapPurchaseToDb() queries.insert( id = payment.id.toString(), mining_fees_sat = payment.miningFees.sat, channel_id = payment.channelId.toByteArray(), tx_id = payment.txId.value.toByteArray(), - lease_type = leaseType, - lease_blob = leaseData, + lease_type = purchase.first, + lease_blob = purchase.second, + payment_details_type = details.first, + payment_details_blob = details.second, created_at = payment.createdAt, confirmed_at = payment.confirmedAt, locked_at = payment.lockedAt, @@ -48,6 +50,11 @@ class InboundLiquidityQueries(val database: PhoenixDatabase) { .executeAsOneOrNull() } + fun getByTxId(txId: TxId): InboundLiquidityOutgoingPayment? { + return queries.getByTxId(tx_id = txId.value.toByteArray(), mapper = Companion::mapPayment) + .executeAsOneOrNull() + } + fun setConfirmed(id: UUID, confirmedAt: Long) { database.transaction { queries.setConfirmed(confirmed_at = confirmedAt, id = id.toString()) @@ -66,8 +73,9 @@ class InboundLiquidityQueries(val database: PhoenixDatabase) { mining_fees_sat: Long, channel_id: ByteArray, tx_id: ByteArray, - lease_type: InboundLiquidityLeaseTypeVersion, + lease_type: InboundLiquidityPurchaseType, lease_blob: ByteArray, + payment_details_blob: ByteArray, created_at: Long, confirmed_at: Long?, locked_at: Long? @@ -77,7 +85,7 @@ class InboundLiquidityQueries(val database: PhoenixDatabase) { miningFees = mining_fees_sat.sat, channelId = channel_id.toByteVector32(), txId = TxId(tx_id), - lease = InboundLiquidityLeaseData.deserialize(lease_type, lease_blob), + purchase = InboundLiquidityPurchaseData.deserialize(lease_type, lease_blob, payment_details_blob), createdAt = created_at, confirmedAt = confirmed_at, lockedAt = locked_at diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt index 9e58443..a4ec7c6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt @@ -27,11 +27,9 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Satoshi import fr.acinq.bitcoin.TxId import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.bin.db.serializers.v1.* import fr.acinq.lightning.db.IncomingPayment -import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer -import fr.acinq.lightning.bin.db.serializers.v1.MilliSatoshiSerializer -import fr.acinq.lightning.bin.db.serializers.v1.UUIDSerializer -import fr.acinq.lightning.bin.db.serializers.v1.SatoshiSerializer +import fr.acinq.lightning.wire.LiquidityAds import io.ktor.utils.io.charsets.* import io.ktor.utils.io.core.* import kotlinx.serialization.* @@ -47,12 +45,21 @@ sealed class IncomingReceivedWithData { @Serializable sealed class Part : IncomingReceivedWithData() { sealed class Htlc : Part() { + @Deprecated("Replaced by [Htlc.V1], which supports the liquidity ads funding fee") @Serializable data class V0( @Serializable val amount: MilliSatoshi, @Serializable val channelId: ByteVector32, val htlcId: Long ) : Htlc() + + @Serializable + data class V1( + val amountReceived: MilliSatoshi, + val channelId: ByteVector32, + val htlcId: Long, + @Serializable(with = FundingFeeSerializer::class) val fundingFee: LiquidityAds.FundingFee?, + ) : Htlc() } sealed class NewChannel : Part() { @@ -100,12 +107,19 @@ sealed class IncomingReceivedWithData { IncomingReceivedWithTypeVersion.MULTIPARTS_V1 -> DbTypesHelper.polymorphicFormat.decodeFromString(SetSerializer(PolymorphicSerializer(Part::class)), json).map { when (it) { is Part.Htlc.V0 -> IncomingPayment.ReceivedWith.LightningPayment( - amount = it.amount, + amountReceived = it.amount, + channelId = it.channelId, + htlcId = it.htlcId, + fundingFee = null + ) + is Part.Htlc.V1 -> IncomingPayment.ReceivedWith.LightningPayment( + amountReceived = it.amountReceived, channelId = it.channelId, - htlcId = it.htlcId + htlcId = it.htlcId, + fundingFee = it.fundingFee ) is Part.NewChannel.V2 -> IncomingPayment.ReceivedWith.NewChannel( - amount = it.amount, + amountReceived = it.amount, serviceFee = it.serviceFee, miningFee = it.miningFee, channelId = it.channelId, @@ -114,7 +128,7 @@ sealed class IncomingReceivedWithData { lockedAt = it.lockedAt, ) is Part.SpliceIn.V0 -> IncomingPayment.ReceivedWith.SpliceIn( - amount = it.amount, + amountReceived = it.amount, serviceFee = it.serviceFee, miningFee = it.miningFee, channelId = it.channelId, @@ -122,8 +136,8 @@ sealed class IncomingReceivedWithData { confirmedAt = it.confirmedAt, lockedAt = it.lockedAt, ) - is Part.FeeCredit.V0 -> IncomingPayment.ReceivedWith.FeeCreditPayment( - amount = it.amount + is Part.FeeCredit.V0 -> IncomingPayment.ReceivedWith.AddedToFeeCredit( + amountReceived = it.amount ) } } @@ -135,13 +149,14 @@ sealed class IncomingReceivedWithData { /** Only serialize received_with into the [IncomingReceivedWithTypeVersion.MULTIPARTS_V1] type. */ fun List.mapToDb(): Pair? = map { when (it) { - is IncomingPayment.ReceivedWith.LightningPayment -> IncomingReceivedWithData.Part.Htlc.V0( - amount = it.amount, + is IncomingPayment.ReceivedWith.LightningPayment -> IncomingReceivedWithData.Part.Htlc.V1( + amountReceived = it.amountReceived, channelId = it.channelId, - htlcId = it.htlcId + htlcId = it.htlcId, + fundingFee = it.fundingFee ) is IncomingPayment.ReceivedWith.NewChannel -> IncomingReceivedWithData.Part.NewChannel.V2( - amount = it.amount, + amount = it.amountReceived, serviceFee = it.serviceFee, miningFee = it.miningFee, channelId = it.channelId, @@ -150,7 +165,7 @@ fun List.mapToDb(): Pair IncomingReceivedWithData.Part.SpliceIn.V0( - amount = it.amount, + amount = it.amountReceived, serviceFee = it.serviceFee, miningFee = it.miningFee, channelId = it.channelId, @@ -158,8 +173,8 @@ fun List.mapToDb(): Pair IncomingReceivedWithData.Part.FeeCredit.V0( - amount = it.amount + is IncomingPayment.ReceivedWith.AddedToFeeCredit -> IncomingReceivedWithData.Part.FeeCredit.V0( + amount = it.amountReceived ) } }.takeIf { it.isNotEmpty() }?.toSet()?.let { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/FundingFeeSerializer.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/FundingFeeSerializer.kt new file mode 100644 index 0000000..1433d7a --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/FundingFeeSerializer.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.lightning.bin.db.serializers.v1 + +import fr.acinq.bitcoin.TxId +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.wire.LiquidityAds +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object TxIdSerializer : AbstractStringSerializer( + name = "TxId", + toString = TxId::toString, + fromString = ::TxId +) + +object FundingFeeSerializer : KSerializer { + + @Serializable + private data class FundingFeeSurrogate( + @Serializable(with = MilliSatoshiSerializer::class) val amount: MilliSatoshi, + @Serializable(with = TxIdSerializer::class) val fundingTxId: TxId + ) + + override val descriptor: SerialDescriptor = FundingFeeSurrogate.serializer().descriptor + + override fun serialize(encoder: Encoder, value: LiquidityAds.FundingFee) { + val surrogate = FundingFeeSurrogate(amount = value.amount, fundingTxId = value.fundingTxId) + return encoder.encodeSerializableValue(FundingFeeSurrogate.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): LiquidityAds.FundingFee { + val surrogate = decoder.decodeSerializableValue(FundingFeeSurrogate.serializer()) + return LiquidityAds.FundingFee(amount = surrogate.amount, fundingTxId = surrogate.fundingTxId) + } +} diff --git a/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq b/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq index d726016..58a3635 100644 --- a/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq +++ b/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq @@ -1,14 +1,20 @@ -import fr.acinq.lightning.bin.db.payments.InboundLiquidityLeaseTypeVersion; +import fr.acinq.lightning.bin.db.payments.InboundLiquidityPurchaseType; +import fr.acinq.lightning.bin.db.payments.InboundLiquidityPaymentDetailsType; -- Stores in a flat row payments standing for an inbound liquidity request (which are done through a splice). --- The lease data are stored in a complex column, as a json-encoded blob. See InboundLiquidityLeaseType file. +-- The purchase data are stored in a complex column, as a json-encoded blob. See InboundLiquidityLeaseType file. +-- +-- Note that these purchase data used to be called "lease" in the old on-the-fly channel mechanism, that's why the +-- colum uses that name, it's a legacy artifact. They now map to a "LiquidityAds.Purchase" object. CREATE TABLE inbound_liquidity_outgoing_payments ( id TEXT NOT NULL PRIMARY KEY, mining_fees_sat INTEGER NOT NULL, channel_id BLOB NOT NULL, tx_id BLOB NOT NULL, - lease_type TEXT AS InboundLiquidityLeaseTypeVersion NOT NULL, + lease_type TEXT AS InboundLiquidityPurchaseType NOT NULL, lease_blob BLOB NOT NULL, + payment_details_type TEXT AS InboundLiquidityPaymentDetailsType NOT NULL, + payment_details_blob BLOB NOT NULL, created_at INTEGER NOT NULL, confirmed_at INTEGER DEFAULT NULL, locked_at INTEGER DEFAULT NULL @@ -16,8 +22,8 @@ CREATE TABLE inbound_liquidity_outgoing_payments ( insert: INSERT INTO inbound_liquidity_outgoing_payments ( - id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, created_at, confirmed_at, locked_at -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, payment_details_type, payment_details_blob, created_at, confirmed_at, locked_at +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); setConfirmed: UPDATE inbound_liquidity_outgoing_payments SET confirmed_at=? WHERE id=?; @@ -26,9 +32,14 @@ setLocked: UPDATE inbound_liquidity_outgoing_payments SET locked_at=? WHERE id=?; get: -SELECT id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, created_at, confirmed_at, locked_at +SELECT id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, payment_details_blob, created_at, confirmed_at, locked_at FROM inbound_liquidity_outgoing_payments WHERE id=?; +getByTxId: +SELECT id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, payment_details_blob, created_at, confirmed_at, locked_at +FROM inbound_liquidity_outgoing_payments +WHERE tx_id=?; + delete: DELETE FROM inbound_liquidity_outgoing_payments WHERE id=?; diff --git a/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm b/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm new file mode 100644 index 0000000..690f424 --- /dev/null +++ b/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm @@ -0,0 +1,12 @@ +import fr.acinq.lightning.bin.db.payments.InboundLiquidityPaymentDetailsType; + +-- Migration: v2 -> v3 +-- +-- With the new on-the-fly channel funding, the liquidity purchase data contain a payment details type that is stored in new columns. +-- +-- Changes: +-- * Added column [payment_details_type] in table [inbound_liquidity_outgoing_payments] +-- * Added column [payment_details_blob] in table [inbound_liquidity_outgoing_payments] + +ALTER TABLE inbound_liquidity_outgoing_payments ADD COLUMN payment_details_type TEXT AS InboundLiquidityPaymentDetailsTypeVersion NOT NULL; +ALTER TABLE inbound_liquidity_outgoing_payments ADD COLUMN payment_details_blob BLOB NOT NULL; diff --git a/src/jvmMain/kotlin/fr/acinq/lightning/bin/Actuals.kt b/src/jvmMain/kotlin/fr/acinq/lightning/bin/Actuals.kt index 0ad311d..ef57dea 100644 --- a/src/jvmMain/kotlin/fr/acinq/lightning/bin/Actuals.kt +++ b/src/jvmMain/kotlin/fr/acinq/lightning/bin/Actuals.kt @@ -15,7 +15,11 @@ import java.util.* actual val datadir: Path = (System.getenv()[PHOENIX_DATADIR]?.toPath() ?: System.getProperty("user.home").toPath().div(".phoenix")) actual fun createAppDbDriver(dir: Path, chain: Chain, nodeId: PublicKey): SqlDriver { - val path = dir / "phoenix.${chain.name.lowercase()}.${nodeId.toHex().take(6)}.db" + val chainName = when (chain) { + is Chain.Testnet3 -> "testnet" + else -> chain.name.lowercase() + } + val path = dir / "phoenix.$chainName.${nodeId.toHex().take(6)}.db" // Initial schema version wasn't set, so we need to set it manually JdbcSqliteDriver("jdbc:sqlite:$path").let { driver -> diff --git a/src/nativeMain/kotlin/fr/acinq/lightning/bin/Actuals.kt b/src/nativeMain/kotlin/fr/acinq/lightning/bin/Actuals.kt index e37835d..10ab956 100644 --- a/src/nativeMain/kotlin/fr/acinq/lightning/bin/Actuals.kt +++ b/src/nativeMain/kotlin/fr/acinq/lightning/bin/Actuals.kt @@ -19,7 +19,11 @@ actual val datadir: Path = setenv("KTOR_LOG_LEVEL", "WARN", 1) getenv(PHOENIX_DATADIR)?.toKString()?.toPath() ?: getenv("HOME")?.toKString()!!.toPath().div(".phoenix") } actual fun createAppDbDriver(dir: Path, chain: Chain, nodeId: PublicKey): SqlDriver { - return NativeSqliteDriver(PhoenixDatabase.Schema, "phoenix.${chain.name.lowercase()}.${nodeId.toHex().take(6)}.db", + val chainName = when (chain) { + is Chain.Testnet3 -> "testnet" + else -> chain.name.lowercase() + } + return NativeSqliteDriver(PhoenixDatabase.Schema, "phoenix.$chainName.${nodeId.toHex().take(6)}.db", onConfiguration = { it.copy(extendedConfig = it.extendedConfig.copy(basePath = dir.toString())) } ) } From 2fd4c0e5d6e200de83f93254d855e36fa20e320f Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:43:06 +0200 Subject: [PATCH 03/13] Rework liquidity-ads purchase database storage Payment details are now stored inside the purchase blob. The payment details type in the db table is just for information purpose and is not typed. It is a free text column. The column storing the purchase/lease type is now also a free text column. If it happens to contains legacy lease data, then we apply a special logic to the blob. The classes used for storing/versioning liquidity ads data are moved in a liquidity ads package. The serialiser for LiquidityAds.FundingFee has been removed (instead we use the same pattern as the other liquidity data). The methods from moving to the canonical data to the versioned db data have been rename: asDb(), asCanonical(). --- .../kotlin/fr/acinq/lightning/bin/Main.kt | 4 - .../bin/db/payments/DbTypesHelper.kt | 10 - .../payments/InboundLiquidityPurchaseType.kt | 213 ------------------ .../db/payments/InboundLiquidityQueries.kt | 25 +- .../db/payments/IncomingReceivedWithType.kt | 10 +- .../payments/liquidityads/FundingFeeData.kt | 28 +++ .../payments/liquidityads/LegacyLeaseData.kt | 62 +++++ .../liquidityads/PaymentDetailsData.kt | 71 ++++++ .../db/payments/liquidityads/PurchaseData.kt | 117 ++++++++++ .../db/serializers/v1/FundingFeeSerializer.kt | 53 ----- .../bin/db/serializers/v1/TxIdSerializer.kt | 25 ++ .../db/InboundLiquidityOutgoingPayments.sq | 16 +- .../sqldelight/phoenixdb/migrations/2.sqm | 5 +- 13 files changed, 335 insertions(+), 304 deletions(-) delete mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityPurchaseType.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/FundingFeeData.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/LegacyLeaseData.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PaymentDetailsData.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt delete mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/FundingFeeSerializer.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/TxIdSerializer.kt diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt index 97f91ca..69858c7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt @@ -274,10 +274,6 @@ class Phoenixd : CliktCommand() { channel_close_outgoing_paymentsAdapter = Channel_close_outgoing_payments.Adapter( closing_info_typeAdapter = EnumColumnAdapter() ), - inbound_liquidity_outgoing_paymentsAdapter = Inbound_liquidity_outgoing_payments.Adapter( - lease_typeAdapter = EnumColumnAdapter(), - payment_details_typeAdapter = EnumColumnAdapter() - ), ) val channelsDb = SqliteChannelsDb(driver, database) val paymentsDb = SqlitePaymentsDb(database) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/DbTypesHelper.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/DbTypesHelper.kt index 316b790..080233a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/DbTypesHelper.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/DbTypesHelper.kt @@ -36,16 +36,6 @@ object DbTypesHelper { subclass(IncomingReceivedWithData.Part.SpliceIn.V0::class) subclass(IncomingReceivedWithData.Part.FeeCredit.V0::class) } - polymorphic(InboundLiquidityPaymentDetailsData::class) { - subclass(InboundLiquidityPaymentDetailsData.ChannelBalance.V0::class) - subclass(InboundLiquidityPaymentDetailsData.FutureHtlc.V0::class) - subclass(InboundLiquidityPaymentDetailsData.FutureHtlcWithPreimage.V0::class) - subclass(InboundLiquidityPaymentDetailsData.ChannelBalanceForFutureHtlc.V0::class) - } - polymorphic(InboundLiquidityPurchaseData::class) { - subclass(InboundLiquidityPurchaseData.Standard.V0::class) - subclass(InboundLiquidityPurchaseData.WithFeeCredit.V0::class) - } } val polymorphicFormat = Json { serializersModule = module } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityPurchaseType.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityPurchaseType.kt deleted file mode 100644 index d85e64d..0000000 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityPurchaseType.kt +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2023 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:UseSerializers( - ByteVectorSerializer::class, - ByteVector32Serializer::class, - ByteVector64Serializer::class, - SatoshiSerializer::class, - MilliSatoshiSerializer::class -) - -package fr.acinq.lightning.bin.db.payments - -import fr.acinq.bitcoin.ByteVector -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.ByteVector64 -import fr.acinq.bitcoin.Satoshi -import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.bin.db.serializers.v1.* -import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment -import fr.acinq.lightning.wire.LiquidityAds -import io.ktor.utils.io.charsets.* -import io.ktor.utils.io.core.* -import kotlinx.serialization.PolymorphicSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers - -enum class InboundLiquidityPurchaseType { - @Deprecated("obsolete with the new on-the-fly channel funding that replaces lease -> purchase") - LEASE_V0, - PURCHASE_STANDARD, - PURCHASE_FEE_CREDIT, -} - -enum class InboundLiquidityPaymentDetailsType { - CHANNEL_BALANCE, - FUTURE_HTLC, - FUTURE_HTLC_WITH_PREIMAGE, - CHANNEL_BALANCE_FUTURE_HTLC, -} - -@Suppress("DEPRECATION") -@Deprecated("obsolete with the new on-the-fly channel funding that replaces lease -> purchase") -sealed class InboundLiquidityLeaseData { - - @Serializable - data class V0( - // these legacy data can still be mapped to the new model - val amount: Satoshi, - val miningFees: Satoshi, - val serviceFee: Satoshi, - // the other legacy data are unused and ignored - val sellerSig: ByteVector64, - val witnessFundingScript: ByteVector, - val witnessLeaseDuration: Int, - val witnessLeaseEnd: Int, - val witnessMaxRelayFeeProportional: Int, - val witnessMaxRelayFeeBase: MilliSatoshi - ) : InboundLiquidityLeaseData() -} - -@Serializable -sealed class InboundLiquidityPaymentDetailsData { - sealed class ChannelBalance : InboundLiquidityPaymentDetailsData() { - @Serializable - data object V0 : ChannelBalance() - } - - sealed class FutureHtlc : InboundLiquidityPaymentDetailsData() { - @Serializable - data class V0(val paymentHashes: List) : FutureHtlc() - } - - sealed class FutureHtlcWithPreimage : InboundLiquidityPaymentDetailsData() { - @Serializable - data class V0(val preimages: List) : FutureHtlcWithPreimage() - } - - sealed class ChannelBalanceForFutureHtlc : InboundLiquidityPaymentDetailsData() { - @Serializable - data class V0(val paymentHashes: List) : ChannelBalanceForFutureHtlc() - } - - companion object { - /** Deserializes a json-encoded blob containing a [LiquidityAds.PaymentDetails] object. */ - fun deserialize(blob: ByteArray): LiquidityAds.PaymentDetails = DbTypesHelper.decodeBlob(blob) { json, _ -> - when (val data = DbTypesHelper.polymorphicFormat.decodeFromString(PolymorphicSerializer(InboundLiquidityPaymentDetailsData::class), json)) { - is ChannelBalance.V0 -> LiquidityAds.PaymentDetails.FromChannelBalance - is FutureHtlc.V0 -> LiquidityAds.PaymentDetails.FromFutureHtlc(data.paymentHashes) - is FutureHtlcWithPreimage.V0 -> LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage(data.preimages) - is ChannelBalanceForFutureHtlc.V0 -> LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(data.paymentHashes) - } - } - } -} - -@Serializable -sealed class InboundLiquidityPurchaseData { - - sealed class Standard : InboundLiquidityPurchaseData() { - @Serializable - data class V0( - val amount: Satoshi, - val miningFees: Satoshi, - val serviceFee: Satoshi, - ) : Standard() - } - - sealed class WithFeeCredit : InboundLiquidityPurchaseData() { - @Serializable - data class V0( - val amount: Satoshi, - val miningFees: Satoshi, - val serviceFee: Satoshi, - val feeCreditUsed: MilliSatoshi, - ) : WithFeeCredit() - } - - companion object { - /** - * Deserializes purchase and payment_details json-encoded blobs into a [LiquidityAds.Purchase] object. - * - * @param typeVersion only used for the legacy leased data, where the blob did not contain the type of the object. The "modern" blobs are expected - * to have been created by a polymorphic serializer, hence they already contain the type and do not need the type_version. - * */ - @Suppress("DEPRECATION") - fun deserialize( - typeVersion: InboundLiquidityPurchaseType, - blob: ByteArray, - paymentDetailsBlob: ByteArray, - ): LiquidityAds.Purchase = DbTypesHelper.decodeBlob(blob) { json, format -> - when (typeVersion) { - // map legacy lease data into the modern [LiquidityAds.Purchase] object ; uses fake payment details data - InboundLiquidityPurchaseType.LEASE_V0 -> format.decodeFromString(json).let { - LiquidityAds.Purchase.Standard( - amount = it.amount, - fees = LiquidityAds.Fees(miningFee = it.miningFees, serviceFee = it.serviceFee), - paymentDetails = LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(ByteVector32.Zeroes)) - ) - } - else -> { - when (val data = DbTypesHelper.polymorphicFormat.decodeFromString(PolymorphicSerializer(InboundLiquidityPurchaseData::class), json)) { - is Standard.V0 -> LiquidityAds.Purchase.Standard( - amount = data.amount, - fees = LiquidityAds.Fees(miningFee = data.miningFees, serviceFee = data.serviceFee), - paymentDetails = InboundLiquidityPaymentDetailsData.deserialize(paymentDetailsBlob) - ) - is WithFeeCredit.V0 -> LiquidityAds.Purchase.WithFeeCredit( - amount = data.amount, - fees = LiquidityAds.Fees(miningFee = data.miningFees, serviceFee = data.serviceFee), - feeCreditUsed = data.feeCreditUsed, - paymentDetails = InboundLiquidityPaymentDetailsData.deserialize(paymentDetailsBlob) - ) - } - } - } - } - } -} - -/** - * Maps a [LiquidityAds.Purchase] object into 2 pairs containing the purchase type+json and the payment details type+json. - * - * Note that payment details are mapped to a separate object because we want to store/view these payment details information - * in a separate column in the database (and not inside the purchase blob). - */ -fun InboundLiquidityOutgoingPayment.mapPurchaseToDb(): Pair, - Pair> { - - // map a [LiquidityAds.PaymentDetails] object into the relevant type/data pair. */ - val (detailsType, detailsData) = when (val d = purchase.paymentDetails) { - is LiquidityAds.PaymentDetails.FromChannelBalance -> InboundLiquidityPaymentDetailsType.CHANNEL_BALANCE to InboundLiquidityPaymentDetailsData.ChannelBalance.V0 - is LiquidityAds.PaymentDetails.FromFutureHtlc -> InboundLiquidityPaymentDetailsType.FUTURE_HTLC to InboundLiquidityPaymentDetailsData.FutureHtlc.V0(d.paymentHashes) - is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> InboundLiquidityPaymentDetailsType.FUTURE_HTLC_WITH_PREIMAGE to InboundLiquidityPaymentDetailsData.FutureHtlcWithPreimage.V0(d.preimages) - is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> InboundLiquidityPaymentDetailsType.CHANNEL_BALANCE_FUTURE_HTLC to InboundLiquidityPaymentDetailsData.ChannelBalanceForFutureHtlc.V0(d.paymentHashes) - } - - // map a [LiquidityAds.Purchase] object into the relevant type/data pair. - val (purchaseType, purchaseData) = when (val p = this.purchase) { - is LiquidityAds.Purchase.Standard -> InboundLiquidityPurchaseType.PURCHASE_STANDARD to InboundLiquidityPurchaseData.Standard.V0( - amount = purchase.amount, - miningFees = purchase.fees.miningFee, - serviceFee = purchase.fees.serviceFee, - ) - - is LiquidityAds.Purchase.WithFeeCredit -> InboundLiquidityPurchaseType.PURCHASE_FEE_CREDIT to InboundLiquidityPurchaseData.WithFeeCredit.V0( - amount = purchase.amount, - miningFees = purchase.fees.miningFee, - serviceFee = purchase.fees.serviceFee, - feeCreditUsed = p.feeCreditUsed, - ) - } - - // encode data with a polymorphic serializer - return (purchaseType to purchaseData.let { - DbTypesHelper.polymorphicFormat.encodeToString(PolymorphicSerializer(InboundLiquidityPurchaseData::class), it).toByteArray(Charsets.UTF_8) - }) to (detailsType to detailsData.let { - DbTypesHelper.polymorphicFormat.encodeToString(PolymorphicSerializer(InboundLiquidityPaymentDetailsData::class), it).toByteArray(Charsets.UTF_8) - }) -} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt index 7ebf768..37fc395 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt @@ -17,10 +17,13 @@ package fr.acinq.lightning.bin.db.payments import fr.acinq.bitcoin.TxId +import fr.acinq.lightning.bin.db.payments.liquidityads.PurchaseData +import fr.acinq.lightning.bin.db.payments.liquidityads.PurchaseData.Companion.encodeForDb import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector32 +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.db.PhoenixDatabase class InboundLiquidityQueries(val database: PhoenixDatabase) { @@ -28,16 +31,22 @@ class InboundLiquidityQueries(val database: PhoenixDatabase) { fun add(payment: InboundLiquidityOutgoingPayment) { database.transaction { - val (purchase, details) = payment.mapPurchaseToDb() queries.insert( id = payment.id.toString(), mining_fees_sat = payment.miningFees.sat, channel_id = payment.channelId.toByteArray(), tx_id = payment.txId.value.toByteArray(), - lease_type = purchase.first, - lease_blob = purchase.second, - payment_details_type = details.first, - payment_details_blob = details.second, + lease_type = when (payment.purchase) { + is LiquidityAds.Purchase.Standard -> "STANDARD" + is LiquidityAds.Purchase.WithFeeCredit -> "WITH_FEE_CREDIT" + }, + lease_blob = payment.purchase.encodeForDb(), + payment_details_type = when (payment.purchase.paymentDetails) { + is LiquidityAds.PaymentDetails.FromChannelBalance -> "FROM_CHANNEL_BALANCE" + is LiquidityAds.PaymentDetails.FromFutureHtlc -> "FROM_FUTURE_HTLC" + is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> "FROM_FUTURE_HTLC_WITH_PREIMAGE" + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> "FROM_CHANNEL_BALANCE_FOR_FUTURE_HTLC" + }, created_at = payment.createdAt, confirmed_at = payment.confirmedAt, locked_at = payment.lockedAt, @@ -73,9 +82,9 @@ class InboundLiquidityQueries(val database: PhoenixDatabase) { mining_fees_sat: Long, channel_id: ByteArray, tx_id: ByteArray, - lease_type: InboundLiquidityPurchaseType, + lease_type: String, lease_blob: ByteArray, - payment_details_blob: ByteArray, + payment_details_type: String?, created_at: Long, confirmed_at: Long?, locked_at: Long? @@ -85,7 +94,7 @@ class InboundLiquidityQueries(val database: PhoenixDatabase) { miningFees = mining_fees_sat.sat, channelId = channel_id.toByteVector32(), txId = TxId(tx_id), - purchase = InboundLiquidityPurchaseData.deserialize(lease_type, lease_blob, payment_details_blob), + purchase = PurchaseData.decodeDataToCanonical(lease_type, lease_blob), createdAt = created_at, confirmedAt = confirmed_at, lockedAt = locked_at diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt index a4ec7c6..5d00aae 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt @@ -27,9 +27,11 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Satoshi import fr.acinq.bitcoin.TxId import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.bin.db.payments.liquidityads.FundingFeeData +import fr.acinq.lightning.bin.db.payments.liquidityads.FundingFeeData.Companion.asCanonical +import fr.acinq.lightning.bin.db.payments.liquidityads.FundingFeeData.Companion.asDb import fr.acinq.lightning.bin.db.serializers.v1.* import fr.acinq.lightning.db.IncomingPayment -import fr.acinq.lightning.wire.LiquidityAds import io.ktor.utils.io.charsets.* import io.ktor.utils.io.core.* import kotlinx.serialization.* @@ -58,7 +60,7 @@ sealed class IncomingReceivedWithData { val amountReceived: MilliSatoshi, val channelId: ByteVector32, val htlcId: Long, - @Serializable(with = FundingFeeSerializer::class) val fundingFee: LiquidityAds.FundingFee?, + val fundingFee: FundingFeeData?, ) : Htlc() } @@ -116,7 +118,7 @@ sealed class IncomingReceivedWithData { amountReceived = it.amountReceived, channelId = it.channelId, htlcId = it.htlcId, - fundingFee = it.fundingFee + fundingFee = it.fundingFee?.asCanonical() ) is Part.NewChannel.V2 -> IncomingPayment.ReceivedWith.NewChannel( amountReceived = it.amount, @@ -153,7 +155,7 @@ fun List.mapToDb(): Pair IncomingReceivedWithData.Part.NewChannel.V2( amount = it.amountReceived, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/FundingFeeData.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/FundingFeeData.kt new file mode 100644 index 0000000..12f146c --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/FundingFeeData.kt @@ -0,0 +1,28 @@ +@file:UseSerializers( + MilliSatoshiSerializer::class, + TxIdSerializer::class, +) + +package fr.acinq.lightning.bin.db.payments.liquidityads + +import fr.acinq.bitcoin.TxId +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.bin.db.serializers.v1.MilliSatoshiSerializer +import fr.acinq.lightning.bin.db.serializers.v1.TxIdSerializer +import fr.acinq.lightning.wire.LiquidityAds +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers + +@Serializable +sealed class FundingFeeData { + + @Serializable + data class V0(val amount: MilliSatoshi, val fundingTxId: TxId) : FundingFeeData() + + companion object { + fun FundingFeeData.asCanonical(): LiquidityAds.FundingFee = when (this) { + is V0 -> LiquidityAds.FundingFee(amount = amount, fundingTxId = fundingTxId) + } + fun LiquidityAds.FundingFee.asDb(): FundingFeeData = V0(amount = amount, fundingTxId = fundingTxId) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/LegacyLeaseData.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/LegacyLeaseData.kt new file mode 100644 index 0000000..bf1b031 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/LegacyLeaseData.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2023 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:UseSerializers( + ByteVectorSerializer::class, + ByteVector32Serializer::class, + ByteVector64Serializer::class, + SatoshiSerializer::class, + MilliSatoshiSerializer::class +) + +package fr.acinq.lightning.bin.db.payments.liquidityads + +import fr.acinq.bitcoin.ByteVector +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.ByteVector64 +import fr.acinq.bitcoin.Satoshi +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.bin.db.serializers.v1.* +import fr.acinq.lightning.wire.LiquidityAds +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers + +enum class InboundLiquidityLeaseType { + @Deprecated("obsolete with the new on-the-fly channel funding that replaces lease -> purchase") + LEASE_V0 +} + +@Suppress("DEPRECATION_WARNING") +@Deprecated("obsolete with the new on-the-fly channel funding that replaces lease with purchase") +@Serializable +data class LeaseV0( + val amount: Satoshi, + val miningFees: Satoshi, + val serviceFee: Satoshi, + val sellerSig: ByteVector64, + val witnessFundingScript: ByteVector, + val witnessLeaseDuration: Int, + val witnessLeaseEnd: Int, + val witnessMaxRelayFeeProportional: Int, + val witnessMaxRelayFeeBase: MilliSatoshi +) { + /** Maps a legacy lease data into the modern [LiquidityAds.Purchase] object using fake payment details data. */ + fun toLiquidityAdsPurchase(): LiquidityAds.Purchase = LiquidityAds.Purchase.Standard( + amount = amount, + fees = LiquidityAds.Fees(miningFee = miningFees, serviceFee = serviceFee), + paymentDetails = LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(ByteVector32.Zeroes)) + ) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PaymentDetailsData.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PaymentDetailsData.kt new file mode 100644 index 0000000..603e355 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PaymentDetailsData.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:UseSerializers( + ByteVectorSerializer::class, + ByteVector32Serializer::class, + ByteVector64Serializer::class, + SatoshiSerializer::class, + MilliSatoshiSerializer::class +) + +package fr.acinq.lightning.bin.db.payments.liquidityads + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.lightning.bin.db.serializers.v1.* +import fr.acinq.lightning.wire.LiquidityAds +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers + + +@Serializable +sealed class PaymentDetailsData { + sealed class ChannelBalance : PaymentDetailsData() { + @Serializable + data object V0 : ChannelBalance() + } + + sealed class FutureHtlc : PaymentDetailsData() { + @Serializable + data class V0(val paymentHashes: List) : FutureHtlc() + } + + sealed class FutureHtlcWithPreimage : PaymentDetailsData() { + @Serializable + data class V0(val preimages: List) : FutureHtlcWithPreimage() + } + + sealed class ChannelBalanceForFutureHtlc : PaymentDetailsData() { + @Serializable + data class V0(val paymentHashes: List) : ChannelBalanceForFutureHtlc() + } + + companion object { + fun PaymentDetailsData.asCanonical(): LiquidityAds.PaymentDetails = when (this) { + is ChannelBalance.V0 -> LiquidityAds.PaymentDetails.FromChannelBalance + is FutureHtlc.V0 -> LiquidityAds.PaymentDetails.FromFutureHtlc(this.paymentHashes) + is FutureHtlcWithPreimage.V0 -> LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage(this.preimages) + is ChannelBalanceForFutureHtlc.V0 -> LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(this.paymentHashes) + } + + fun LiquidityAds.PaymentDetails.asDb(): PaymentDetailsData = when (this) { + is LiquidityAds.PaymentDetails.FromChannelBalance -> ChannelBalance.V0 + is LiquidityAds.PaymentDetails.FromFutureHtlc -> FutureHtlc.V0(this.paymentHashes) + is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> FutureHtlcWithPreimage.V0(this.preimages) + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> ChannelBalanceForFutureHtlc.V0(this.paymentHashes) + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt new file mode 100644 index 0000000..a8d29d2 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2023 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:UseSerializers( + ByteVectorSerializer::class, + ByteVector32Serializer::class, + ByteVector64Serializer::class, + SatoshiSerializer::class, + MilliSatoshiSerializer::class +) + +package fr.acinq.lightning.bin.db.payments.liquidityads + +import fr.acinq.bitcoin.ByteVector +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.ByteVector64 +import fr.acinq.bitcoin.Satoshi +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.bin.db.payments.DbTypesHelper +import fr.acinq.lightning.bin.db.payments.liquidityads.PaymentDetailsData.Companion.asCanonical +import fr.acinq.lightning.bin.db.payments.liquidityads.PaymentDetailsData.Companion.asDb +import fr.acinq.lightning.bin.db.serializers.v1.ByteVectorSerializer +import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer +import fr.acinq.lightning.bin.db.serializers.v1.ByteVector64Serializer +import fr.acinq.lightning.bin.db.serializers.v1.SatoshiSerializer +import fr.acinq.lightning.bin.db.serializers.v1.MilliSatoshiSerializer +import fr.acinq.lightning.wire.LiquidityAds +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.text.toByteArray + + +sealed class PurchaseData { + sealed class Standard : PurchaseData() { + @Serializable + data class V0( + val amount: Satoshi, + val miningFees: Satoshi, + val serviceFee: Satoshi, + val paymentDetails: PaymentDetailsData, + ) : Standard() + } + sealed class WithFeeCredit : PurchaseData() { + @Serializable + data class V0( + val amount: Satoshi, + val miningFees: Satoshi, + val serviceFee: Satoshi, + val feeCreditUsed: MilliSatoshi, + val paymentDetails: PaymentDetailsData, + ) : WithFeeCredit() + } + + companion object { + private fun PurchaseData.asCanonical(): LiquidityAds.Purchase = when (this) { + is Standard.V0 -> LiquidityAds.Purchase.Standard( + amount = amount, + fees = LiquidityAds.Fees(miningFee = miningFees, serviceFee = serviceFee), + paymentDetails = paymentDetails.asCanonical() + ) + is WithFeeCredit.V0 -> LiquidityAds.Purchase.WithFeeCredit( + amount = amount, + fees = LiquidityAds.Fees(miningFee = miningFees, serviceFee = serviceFee), + feeCreditUsed = feeCreditUsed, + paymentDetails = paymentDetails.asCanonical() + ) + } + + private fun LiquidityAds.Purchase.asDb(): PurchaseData = when (val value = this) { + is LiquidityAds.Purchase.Standard -> Standard.V0( + amount = value.amount, + miningFees = value.fees.miningFee, + serviceFee = value.fees.serviceFee, + paymentDetails = value.paymentDetails.asDb() + ) + is LiquidityAds.Purchase.WithFeeCredit -> WithFeeCredit.V0( + amount = value.amount, value.fees.miningFee, + serviceFee = value.fees.serviceFee, + paymentDetails = value.paymentDetails.asDb(), + feeCreditUsed = value.feeCreditUsed + ) + } + + /** + * Deserializes a json-encoded blob into a [LiquidityAds.Purchase] object. + * + * @param typeVersion only used for the legacy leased data, where the blob did not contain the type of the object. + */ + @Suppress("DEPRECATION") + fun decodeDataToCanonical( + typeVersion: String, + blob: ByteArray, + ): LiquidityAds.Purchase = DbTypesHelper.decodeBlob(blob) { json, format -> + when (typeVersion) { + InboundLiquidityLeaseType.LEASE_V0.name -> format.decodeFromString(json).toLiquidityAdsPurchase() + else -> format.decodeFromString(json).asCanonical() + } + } + + fun LiquidityAds.Purchase.encodeForDb(): ByteArray = Json.encodeToString(this.asDb()).toByteArray(Charsets.UTF_8) + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/FundingFeeSerializer.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/FundingFeeSerializer.kt deleted file mode 100644 index 1433d7a..0000000 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/FundingFeeSerializer.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2024 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.lightning.bin.db.serializers.v1 - -import fr.acinq.bitcoin.TxId -import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.wire.LiquidityAds -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -object TxIdSerializer : AbstractStringSerializer( - name = "TxId", - toString = TxId::toString, - fromString = ::TxId -) - -object FundingFeeSerializer : KSerializer { - - @Serializable - private data class FundingFeeSurrogate( - @Serializable(with = MilliSatoshiSerializer::class) val amount: MilliSatoshi, - @Serializable(with = TxIdSerializer::class) val fundingTxId: TxId - ) - - override val descriptor: SerialDescriptor = FundingFeeSurrogate.serializer().descriptor - - override fun serialize(encoder: Encoder, value: LiquidityAds.FundingFee) { - val surrogate = FundingFeeSurrogate(amount = value.amount, fundingTxId = value.fundingTxId) - return encoder.encodeSerializableValue(FundingFeeSurrogate.serializer(), surrogate) - } - - override fun deserialize(decoder: Decoder): LiquidityAds.FundingFee { - val surrogate = decoder.decodeSerializableValue(FundingFeeSurrogate.serializer()) - return LiquidityAds.FundingFee(amount = surrogate.amount, fundingTxId = surrogate.fundingTxId) - } -} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/TxIdSerializer.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/TxIdSerializer.kt new file mode 100644 index 0000000..de17a5f --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/serializers/v1/TxIdSerializer.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.lightning.bin.db.serializers.v1 + +import fr.acinq.bitcoin.TxId + +object TxIdSerializer : AbstractStringSerializer( + name = "TxId", + toString = TxId::toString, + fromString = ::TxId +) diff --git a/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq b/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq index 58a3635..6e3f7d0 100644 --- a/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq +++ b/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq @@ -1,5 +1,4 @@ -import fr.acinq.lightning.bin.db.payments.InboundLiquidityPurchaseType; -import fr.acinq.lightning.bin.db.payments.InboundLiquidityPaymentDetailsType; +import fr.acinq.lightning.bin.db.payments.liquidityads.InboundLiquidityPurchaseType; -- Stores in a flat row payments standing for an inbound liquidity request (which are done through a splice). -- The purchase data are stored in a complex column, as a json-encoded blob. See InboundLiquidityLeaseType file. @@ -11,10 +10,9 @@ CREATE TABLE inbound_liquidity_outgoing_payments ( mining_fees_sat INTEGER NOT NULL, channel_id BLOB NOT NULL, tx_id BLOB NOT NULL, - lease_type TEXT AS InboundLiquidityPurchaseType NOT NULL, + lease_type TEXT NOT NULL, lease_blob BLOB NOT NULL, - payment_details_type TEXT AS InboundLiquidityPaymentDetailsType NOT NULL, - payment_details_blob BLOB NOT NULL, + payment_details_type TEXT DEFAULT NULL, created_at INTEGER NOT NULL, confirmed_at INTEGER DEFAULT NULL, locked_at INTEGER DEFAULT NULL @@ -22,8 +20,8 @@ CREATE TABLE inbound_liquidity_outgoing_payments ( insert: INSERT INTO inbound_liquidity_outgoing_payments ( - id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, payment_details_type, payment_details_blob, created_at, confirmed_at, locked_at -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, payment_details_type, created_at, confirmed_at, locked_at +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); setConfirmed: UPDATE inbound_liquidity_outgoing_payments SET confirmed_at=? WHERE id=?; @@ -32,12 +30,12 @@ setLocked: UPDATE inbound_liquidity_outgoing_payments SET locked_at=? WHERE id=?; get: -SELECT id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, payment_details_blob, created_at, confirmed_at, locked_at +SELECT id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, payment_details_type, created_at, confirmed_at, locked_at FROM inbound_liquidity_outgoing_payments WHERE id=?; getByTxId: -SELECT id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, payment_details_blob, created_at, confirmed_at, locked_at +SELECT id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, payment_details_type, created_at, confirmed_at, locked_at FROM inbound_liquidity_outgoing_payments WHERE tx_id=?; diff --git a/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm b/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm index 690f424..03b9184 100644 --- a/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm +++ b/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm @@ -1,4 +1,4 @@ -import fr.acinq.lightning.bin.db.payments.InboundLiquidityPaymentDetailsType; +import fr.acinq.lightning.bin.db.payments.liquidityads.InboundLiquidityPaymentDetailsType; -- Migration: v2 -> v3 -- @@ -8,5 +8,4 @@ import fr.acinq.lightning.bin.db.payments.InboundLiquidityPaymentDetailsType; -- * Added column [payment_details_type] in table [inbound_liquidity_outgoing_payments] -- * Added column [payment_details_blob] in table [inbound_liquidity_outgoing_payments] -ALTER TABLE inbound_liquidity_outgoing_payments ADD COLUMN payment_details_type TEXT AS InboundLiquidityPaymentDetailsTypeVersion NOT NULL; -ALTER TABLE inbound_liquidity_outgoing_payments ADD COLUMN payment_details_blob BLOB NOT NULL; +ALTER TABLE inbound_liquidity_outgoing_payments ADD COLUMN payment_details_type TEXT DEFAULT NULL; From 075d08e9270cbdec803790c303463a2f19069490 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:57:30 +0200 Subject: [PATCH 04/13] Fix naming for consistency, and removed unused imports --- .../db/payments/InboundLiquidityQueries.kt | 6 +++--- .../liquidityads/PaymentDetailsData.kt | 4 ---- .../db/payments/liquidityads/PurchaseData.kt | 19 ++++--------------- .../db/InboundLiquidityOutgoingPayments.sq | 2 -- .../sqldelight/phoenixdb/migrations/2.sqm | 5 ----- 5 files changed, 7 insertions(+), 29 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt index 37fc395..b8d3083 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt @@ -18,7 +18,7 @@ package fr.acinq.lightning.bin.db.payments import fr.acinq.bitcoin.TxId import fr.acinq.lightning.bin.db.payments.liquidityads.PurchaseData -import fr.acinq.lightning.bin.db.payments.liquidityads.PurchaseData.Companion.encodeForDb +import fr.acinq.lightning.bin.db.payments.liquidityads.PurchaseData.Companion.encodeAsDb import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.utils.sat @@ -40,7 +40,7 @@ class InboundLiquidityQueries(val database: PhoenixDatabase) { is LiquidityAds.Purchase.Standard -> "STANDARD" is LiquidityAds.Purchase.WithFeeCredit -> "WITH_FEE_CREDIT" }, - lease_blob = payment.purchase.encodeForDb(), + lease_blob = payment.purchase.encodeAsDb(), payment_details_type = when (payment.purchase.paymentDetails) { is LiquidityAds.PaymentDetails.FromChannelBalance -> "FROM_CHANNEL_BALANCE" is LiquidityAds.PaymentDetails.FromFutureHtlc -> "FROM_FUTURE_HTLC" @@ -94,7 +94,7 @@ class InboundLiquidityQueries(val database: PhoenixDatabase) { miningFees = mining_fees_sat.sat, channelId = channel_id.toByteVector32(), txId = TxId(tx_id), - purchase = PurchaseData.decodeDataToCanonical(lease_type, lease_blob), + purchase = PurchaseData.decodeAsCanonical(lease_type, lease_blob), createdAt = created_at, confirmedAt = confirmed_at, lockedAt = locked_at diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PaymentDetailsData.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PaymentDetailsData.kt index 603e355..65737d5 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PaymentDetailsData.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PaymentDetailsData.kt @@ -15,11 +15,7 @@ */ @file:UseSerializers( - ByteVectorSerializer::class, ByteVector32Serializer::class, - ByteVector64Serializer::class, - SatoshiSerializer::class, - MilliSatoshiSerializer::class ) package fr.acinq.lightning.bin.db.payments.liquidityads diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt index a8d29d2..ca39292 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 ACINQ SAS + * Copyright 2024 ACINQ SAS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,34 +15,23 @@ */ @file:UseSerializers( - ByteVectorSerializer::class, - ByteVector32Serializer::class, - ByteVector64Serializer::class, SatoshiSerializer::class, MilliSatoshiSerializer::class ) package fr.acinq.lightning.bin.db.payments.liquidityads -import fr.acinq.bitcoin.ByteVector -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.ByteVector64 import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.bin.db.payments.DbTypesHelper import fr.acinq.lightning.bin.db.payments.liquidityads.PaymentDetailsData.Companion.asCanonical import fr.acinq.lightning.bin.db.payments.liquidityads.PaymentDetailsData.Companion.asDb -import fr.acinq.lightning.bin.db.serializers.v1.ByteVectorSerializer -import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer -import fr.acinq.lightning.bin.db.serializers.v1.ByteVector64Serializer -import fr.acinq.lightning.bin.db.serializers.v1.SatoshiSerializer -import fr.acinq.lightning.bin.db.serializers.v1.MilliSatoshiSerializer +import fr.acinq.lightning.bin.db.serializers.v1.* import fr.acinq.lightning.wire.LiquidityAds import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import kotlin.text.toByteArray sealed class PurchaseData { @@ -102,7 +91,7 @@ sealed class PurchaseData { * @param typeVersion only used for the legacy leased data, where the blob did not contain the type of the object. */ @Suppress("DEPRECATION") - fun decodeDataToCanonical( + fun decodeAsCanonical( typeVersion: String, blob: ByteArray, ): LiquidityAds.Purchase = DbTypesHelper.decodeBlob(blob) { json, format -> @@ -112,6 +101,6 @@ sealed class PurchaseData { } } - fun LiquidityAds.Purchase.encodeForDb(): ByteArray = Json.encodeToString(this.asDb()).toByteArray(Charsets.UTF_8) + fun LiquidityAds.Purchase.encodeAsDb(): ByteArray = Json.encodeToString(this.asDb()).toByteArray(Charsets.UTF_8) } } diff --git a/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq b/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq index 6e3f7d0..e2ae404 100644 --- a/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq +++ b/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/InboundLiquidityOutgoingPayments.sq @@ -1,5 +1,3 @@ -import fr.acinq.lightning.bin.db.payments.liquidityads.InboundLiquidityPurchaseType; - -- Stores in a flat row payments standing for an inbound liquidity request (which are done through a splice). -- The purchase data are stored in a complex column, as a json-encoded blob. See InboundLiquidityLeaseType file. -- diff --git a/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm b/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm index 03b9184..f66391e 100644 --- a/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm +++ b/src/commonMain/sqldelight/phoenixdb/migrations/2.sqm @@ -1,11 +1,6 @@ -import fr.acinq.lightning.bin.db.payments.liquidityads.InboundLiquidityPaymentDetailsType; - -- Migration: v2 -> v3 -- --- With the new on-the-fly channel funding, the liquidity purchase data contain a payment details type that is stored in new columns. --- -- Changes: -- * Added column [payment_details_type] in table [inbound_liquidity_outgoing_payments] --- * Added column [payment_details_blob] in table [inbound_liquidity_outgoing_payments] ALTER TABLE inbound_liquidity_outgoing_payments ADD COLUMN payment_details_type TEXT DEFAULT NULL; From bea5be61ba46b517ebe5d716bb89c17c669ab9aa Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 20 Sep 2024 17:00:51 +0200 Subject: [PATCH 05/13] add liquidity logs --- .../kotlin/fr/acinq/lightning/bin/Main.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt index 69858c7..2b711f4 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt @@ -40,14 +40,13 @@ import fr.acinq.lightning.bin.logs.stringTimestamp import fr.acinq.lightning.blockchain.mempool.MempoolSpaceClient import fr.acinq.lightning.blockchain.mempool.MempoolSpaceWatcher import fr.acinq.lightning.crypto.LocalKeyManager -import fr.acinq.lightning.db.ChannelsDb -import fr.acinq.lightning.db.Databases -import fr.acinq.lightning.db.PaymentsDb +import fr.acinq.lightning.db.* import fr.acinq.lightning.io.Peer import fr.acinq.lightning.io.TcpSocket import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.payment.LiquidityPolicy import fr.acinq.lightning.utils.* +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.db.* import io.ktor.http.* import io.ktor.server.application.* @@ -319,10 +318,18 @@ class Phoenixd : CliktCommand() { } launch { nodeParams.nodeEvents - .filterIsInstance() - .filter { it.amount > 0.msat } + .filterIsInstance() .collect { - consoleLog("received lightning payment: ${it.amount.truncateToSatoshi()} (${it.receivedWith.joinToString { part -> part::class.simpleName.toString().lowercase() }})") + when (it) { + is PaymentEvents.PaymentReceived -> + consoleLog("received lightning payment: ${it.amount.truncateToSatoshi()} (${it.receivedWith.joinToString { part -> part::class.simpleName.toString().lowercase() }})") + is PaymentEvents.PaymentSent -> + when(val payment = it.payment) { + is InboundLiquidityOutgoingPayment -> + consoleLog("purchased inbound liquidity: ${payment.purchase.amount} (fee=${payment.fees.truncateToSatoshi()} feeCreditUsed=${(payment.purchase as? LiquidityAds.Purchase.WithFeeCredit)?.feeCreditUsed?.truncateToSatoshi() ?: 0.sat} type=${payment.purchase.paymentDetails.paymentType::class.simpleName.toString().lowercase()})") + else -> {} + } + } } } launch { @@ -334,9 +341,9 @@ class Phoenixd : CliktCommand() { // consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over max fee credit (max=${reason.maxAllowedCredit})")) // } is LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee -> - consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over max mining fee (max=${reason.maxAbsoluteFee})")) + consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over absolute fee (fee=${it.fee.truncateToSatoshi()} max=${reason.maxAbsoluteFee})")) is LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee -> - consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): fee=${it.fee.truncateToSatoshi()} more than ${reason.maxRelativeFeeBasisPoints.toDouble() / 100}% of amount")) + consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over relative fee (fee=${it.fee.truncateToSatoshi()} max=${reason.maxRelativeFeeBasisPoints.toDouble() / 100}%")) LiquidityEvents.Rejected.Reason.PolicySetToDisabled -> consoleLog(yellow("automated liquidity is disabled")) LiquidityEvents.Rejected.Reason.ChannelFundingInProgress -> From 0e41de1a01918a5971870f9910e979cb9cdb9a76 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 20 Sep 2024 17:52:24 +0200 Subject: [PATCH 06/13] fixup! Fix naming for consistency, and removed unused imports --- .../lightning/bin/db/payments/liquidityads/PurchaseData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt index ca39292..e3936cf 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt @@ -33,7 +33,7 @@ import kotlinx.serialization.UseSerializers import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json - +@Serializable sealed class PurchaseData { sealed class Standard : PurchaseData() { @Serializable From 6cc10663a5c3a74262abbff5c9945500c9b269dc Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 27 Sep 2024 11:26:01 +0200 Subject: [PATCH 07/13] fixup! initial integration --- src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt index 49f2561..4d0cc7e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt @@ -27,6 +27,7 @@ import fr.acinq.lightning.bin.payments.lnurl.models.LnurlWithdraw import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelCommand +import fr.acinq.lightning.channel.ChannelFundingResponse import fr.acinq.lightning.channel.states.ChannelStateWithCommitments import fr.acinq.lightning.channel.states.Closed import fr.acinq.lightning.channel.states.Closing @@ -394,8 +395,8 @@ class Api( }.toEither() when (res) { is Either.Right -> when (val r = res.value) { - is ChannelCommand.Commitment.Splice.Response.Created -> call.respondText(r.fundingTxId.toString()) - is ChannelCommand.Commitment.Splice.Response.Failure -> call.respondText(r.toString()) + is ChannelFundingResponse.Success -> call.respondText(r.fundingTxId.toString()) + is ChannelFundingResponse.Failure -> call.respondText(r.toString()) else -> call.respondText("no channel available") } is Either.Left -> call.respondText(res.value.message.toString()) From 3dc19c3bd3038d2640cf63e204861197c0e9762f Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 27 Sep 2024 11:29:32 +0200 Subject: [PATCH 08/13] suppress warnings --- .../acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt | 2 +- .../acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt index b8d3083..835d8a8 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/InboundLiquidityQueries.kt @@ -84,7 +84,7 @@ class InboundLiquidityQueries(val database: PhoenixDatabase) { tx_id: ByteArray, lease_type: String, lease_blob: ByteArray, - payment_details_type: String?, + @Suppress("UNUSED_PARAMETER") payment_details_type: String?, created_at: Long, confirmed_at: Long?, locked_at: Long? diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt index 5d00aae..c716f0b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/IncomingReceivedWithType.kt @@ -107,6 +107,7 @@ sealed class IncomingReceivedWithData { ): List = DbTypesHelper.decodeBlob(blob) { json, _ -> when (typeVersion) { IncomingReceivedWithTypeVersion.MULTIPARTS_V1 -> DbTypesHelper.polymorphicFormat.decodeFromString(SetSerializer(PolymorphicSerializer(Part::class)), json).map { + @Suppress("DEPRECATION") when (it) { is Part.Htlc.V0 -> IncomingPayment.ReceivedWith.LightningPayment( amountReceived = it.amount, From 9f52516ee76ccbe0ac13412bf4f0aca1577f0ffb Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 27 Sep 2024 11:36:25 +0200 Subject: [PATCH 09/13] display fee credit as sat --- src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt index 2b711f4..4ecf579 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt @@ -337,6 +337,7 @@ class Phoenixd : CliktCommand() { .filterIsInstance() .collect { when (val reason = it.reason) { + // TODO: put this back after rework of LiquidityPolicy to handle fee credit // is LiquidityEvents.Rejected.Reason.OverMaxCredit -> { // consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over max fee credit (max=${reason.maxAllowedCredit})")) // } @@ -360,7 +361,7 @@ class Phoenixd : CliktCommand() { launch { peer.feeCreditFlow .drop(1) // we drop the initial value which is 0 msat - .collect { feeCredit -> consoleLog("fee credit: $feeCredit") } + .collect { feeCredit -> consoleLog("fee credit: ${feeCredit.truncateToSatoshi()}") } } } From f713508feedd0df492d02916759bc63e05dd4579 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 27 Sep 2024 11:42:53 +0200 Subject: [PATCH 10/13] fixup! Rework liquidity-ads purchase database storage --- .../lightning/bin/db/payments/liquidityads/PurchaseData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt index e3936cf..1bf551b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/liquidityads/PurchaseData.kt @@ -101,6 +101,6 @@ sealed class PurchaseData { } } - fun LiquidityAds.Purchase.encodeAsDb(): ByteArray = Json.encodeToString(this.asDb()).toByteArray(Charsets.UTF_8) + fun LiquidityAds.Purchase.encodeAsDb(): ByteArray = Json.encodeToString(this.asDb()).encodeToByteArray() } } From e0af890cd8af5ad00034f0fba3823a94c8a5e0f8 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 27 Sep 2024 17:23:31 +0200 Subject: [PATCH 11/13] more detailed fee breakdown Leverages improvements in https://github.com/ACINQ/lightning-kmp/pull/706. --- .../kotlin/fr/acinq/lightning/bin/Main.kt | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt index 4ecf579..d184c29 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt @@ -321,14 +321,23 @@ class Phoenixd : CliktCommand() { .filterIsInstance() .collect { when (it) { - is PaymentEvents.PaymentReceived -> - consoleLog("received lightning payment: ${it.amount.truncateToSatoshi()} (${it.receivedWith.joinToString { part -> part::class.simpleName.toString().lowercase() }})") - is PaymentEvents.PaymentSent -> - when(val payment = it.payment) { - is InboundLiquidityOutgoingPayment -> - consoleLog("purchased inbound liquidity: ${payment.purchase.amount} (fee=${payment.fees.truncateToSatoshi()} feeCreditUsed=${(payment.purchase as? LiquidityAds.Purchase.WithFeeCredit)?.feeCreditUsed?.truncateToSatoshi() ?: 0.sat} type=${payment.purchase.paymentDetails.paymentType::class.simpleName.toString().lowercase()})") - else -> {} + is PaymentEvents.PaymentReceived -> { + val fee = it.receivedWith.filterIsInstance().map { it.fundingFee?.amount ?: 0.msat }.sum().truncateToSatoshi() + val type = it.receivedWith.joinToString { part -> part::class.simpleName.toString().lowercase() } + consoleLog("received lightning payment: ${it.amount.truncateToSatoshi()} ($type${if (fee > 0.sat) " fee=$fee" else ""})") } + is PaymentEvents.PaymentSent -> + when (val payment = it.payment) { + is InboundLiquidityOutgoingPayment -> { + val totalFee = payment.fees.truncateToSatoshi() + val feePaidFromBalance = payment.feePaidFromChannelBalance.total + val feePaidFromFeeCredit = (payment.purchase as? LiquidityAds.Purchase.WithFeeCredit)?.feeCreditUsed?.truncateToSatoshi() ?: 0.sat + val feeRemaining = totalFee - feePaidFromBalance - feePaidFromFeeCredit + val purchaseType = payment.purchase.paymentDetails.paymentType::class.simpleName.toString().lowercase() + consoleLog("purchased inbound liquidity: ${payment.purchase.amount} (totalFee=$totalFee feePaidFromBalance=$feePaidFromBalance feePaidFromFeeCredit=$feePaidFromFeeCredit feeRemaining=$feeRemaining purchaseType=$purchaseType)") + } + else -> {} + } } } } @@ -344,7 +353,7 @@ class Phoenixd : CliktCommand() { is LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee -> consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over absolute fee (fee=${it.fee.truncateToSatoshi()} max=${reason.maxAbsoluteFee})")) is LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee -> - consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over relative fee (fee=${it.fee.truncateToSatoshi()} max=${reason.maxRelativeFeeBasisPoints.toDouble() / 100}%")) + consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over relative fee (fee=${it.fee.truncateToSatoshi()} max=${reason.maxRelativeFeeBasisPoints.toDouble() / 100}%)")) LiquidityEvents.Rejected.Reason.PolicySetToDisabled -> consoleLog(yellow("automated liquidity is disabled")) LiquidityEvents.Rejected.Reason.ChannelFundingInProgress -> From 15359681873ac8b45e8c6594f016617e0922dde1 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 27 Sep 2024 17:52:51 +0200 Subject: [PATCH 12/13] fixup! more detailed fee breakdown --- src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt index d184c29..fdc90a0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt @@ -331,7 +331,7 @@ class Phoenixd : CliktCommand() { is InboundLiquidityOutgoingPayment -> { val totalFee = payment.fees.truncateToSatoshi() val feePaidFromBalance = payment.feePaidFromChannelBalance.total - val feePaidFromFeeCredit = (payment.purchase as? LiquidityAds.Purchase.WithFeeCredit)?.feeCreditUsed?.truncateToSatoshi() ?: 0.sat + val feePaidFromFeeCredit = payment.feeCreditUsed.truncateToSatoshi() val feeRemaining = totalFee - feePaidFromBalance - feePaidFromFeeCredit val purchaseType = payment.purchase.paymentDetails.paymentType::class.simpleName.toString().lowercase() consoleLog("purchased inbound liquidity: ${payment.purchase.amount} (totalFee=$totalFee feePaidFromBalance=$feePaidFromBalance feePaidFromFeeCredit=$feePaidFromFeeCredit feeRemaining=$feeRemaining purchaseType=$purchaseType)") From aa3e9424a794d93b45372c0a2b71a9d621a6d751 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 3 Oct 2024 14:14:29 +0200 Subject: [PATCH 13/13] use lightning-kmp 1.8.0 --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index c5a060e..7d046bb 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,6 +1,6 @@ object Versions { val kotlin = "1.9.23" - val lightningKmp = "1.7.4-SNAPSHOT" + val lightningKmp = "1.8.0" val sqlDelight = "2.0.1" val okio = "3.8.0" val clikt = "4.2.2"