From 3bb9dc4eff2ac48a4828f5062533a6e0ace59a02 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2024 16:06:43 +0200 Subject: [PATCH] add estimateliquidityfees Returns a best-effort *estimate* of liquidity fees for a given amount. Note that: - it depends on the current mining feerate, which is volatile - this estimate is the full cost, it doesn't take into account the `feeCredit` (which will be deduced) - if you receive `A sat` with a setting of `--auto-liquidity B`, then the actual liquidity amount will be A+B. ``` ./phoenix-cli estimateliquidityfees --amountSat 2000000 { "miningFeeSat": 1219, "serviceFeeSat": 20000 } ``` See #88,#104. --- .../kotlin/fr/acinq/lightning/bin/Api.kt | 9 ++++ .../kotlin/fr/acinq/lightning/bin/conf/Lsp.kt | 54 +++++++++++-------- .../lightning/bin/json/JsonSerializers.kt | 6 +++ .../fr/acinq/lightning/cli/PhoenixCli.kt | 12 +++++ 4 files changed, 59 insertions(+), 22 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt index 94ed4f4..e6ba690 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt @@ -11,6 +11,7 @@ import fr.acinq.lightning.BuildVersions import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.NodeParams import fr.acinq.lightning.bin.api.WebsocketProtocolAuthenticationProvider +import fr.acinq.lightning.bin.conf.LSP import fr.acinq.lightning.bin.db.SqlitePaymentsDb import fr.acinq.lightning.bin.db.WalletPaymentId import fr.acinq.lightning.bin.json.ApiType.* @@ -56,6 +57,8 @@ import io.ktor.server.routing.* import io.ktor.server.websocket.* import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json @@ -161,6 +164,12 @@ class Api( .sum().truncateToSatoshi() call.respond(Balance(balance, nodeParams.feeCredit.value)) } + 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)) + } get("listchannels") { call.respond(peer.channels.values.toList()) } 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 24ecf23..4c43db7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt @@ -2,9 +2,12 @@ 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) { @@ -53,27 +56,34 @@ data class LSP(val walletParams: WalletParams, val swapInXpub: String) { else -> error("unsupported chain $chain") } -// 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 -// ) -// } + 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/json/JsonSerializers.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt index ea37d22..22819c2 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt @@ -29,6 +29,7 @@ import fr.acinq.lightning.json.JsonSerializers import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.payment.OfferPaymentMetadata import fr.acinq.lightning.utils.UUID +import fr.acinq.lightning.wire.LiquidityAds import io.ktor.http.* import kotlinx.datetime.Clock import kotlinx.serialization.SerialName @@ -74,6 +75,11 @@ sealed class ApiType { @Serializable data class Balance(@SerialName("balanceSat") val amount: Satoshi, @SerialName("feeCreditSat") val feeCredit: Satoshi) : 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) + } + @Serializable data class GeneratedInvoice(@SerialName("amountSat") val amount: Satoshi?, val paymentHash: ByteVector32, val serialized: String) : ApiType() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt b/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt index d24beac..0d4f95a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt @@ -47,6 +47,7 @@ fun main(args: Array) = .subcommands( GetInfo(), GetBalance(), + EstimateLiquidityFees(), ListChannels(), GetOutgoingPayment(), ListOutgoingPayments(), @@ -139,6 +140,17 @@ class GetBalance : PhoenixCliCommand(name = "getbalance", help = "Returns your c } } +class EstimateLiquidityFees : PhoenixCliCommand(name = "estimateliquidityfees", help = "Estimates the liquidity fees for a given amount, at current feerates.") { + private val amountSat by option("--amountSat").long().required() + override suspend fun httpRequest() = commonOptions.httpClient.use { + it.get(url = commonOptions.baseUrl / "estimateliquidityfees") { + url { + parameters.append("amountSat", amountSat.toString()) + } + } + } +} + class ListChannels : PhoenixCliCommand(name = "listchannels", help = "List all channels") { override suspend fun httpRequest() = commonOptions.httpClient.use { it.get(url = commonOptions.baseUrl / "listchannels")