Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add method to calculate input for a trampoline fee #589

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import fr.acinq.lightning.Lightning.nodeFee
import fr.acinq.lightning.blockchain.fee.FeerateTolerance
import fr.acinq.lightning.blockchain.fee.OnChainFeeConf
import fr.acinq.lightning.crypto.KeyManager
import fr.acinq.lightning.io.SendPayment
import fr.acinq.lightning.payment.LiquidityPolicy
import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.utils.sat
Expand All @@ -25,7 +26,36 @@ data class NodeUri(val id: PublicKey, val host: String, val port: Int)
* This class encapsulates the fees and expiry to use at a particular attempt.
*/
data class TrampolineFees(val feeBase: Satoshi, val feeProportional: Long, val cltvExpiryDelta: CltvExpiryDelta) {
/**
* Calculates the trampoline fee for [recipientAmount].
*
* @param recipientAmount the amount that will be received by the recipient.
*/
fun calculateFees(recipientAmount: MilliSatoshi): MilliSatoshi = nodeFee(feeBase.toMilliSatoshi(), feeProportional, recipientAmount)

/**
* Calculates the amount that must be provided to the trampoline payload to pay the trampoline
* fee and consume all the [finalAmount], leaving no dust.
*
* E.g.: balance is 1_000 sat, trampoline fees is 4 sat + 0.4%. To use up all balance, [SendPayment]
* should use an amount of 992_032 msat (fee=7_968 msat, around 4 sat + 0.4%).
*
* @param finalAmount what leaves the node, including fees.
* @return the amount to provide to the trampoline handler, null if there is no valid amount.
*/
fun calculateReverseAmount(finalAmount: MilliSatoshi): MilliSatoshi? {
val amountBeforeBaseFee = finalAmount - this.feeBase.toMilliSatoshi()
val proportionalFee = this.feeProportional.toDouble() / 1_000_000

// first, we approximate the amount that must be used in the trampoline payload
val amountBeforeFee = amountBeforeBaseFee.msat / (1 + proportionalFee)

// we cannot return the amount above directly: `OutgoingPaymentHandler` uses `calculateFees` to
// create the trampoline payload, which involves rounding, so the amount leaving the wallet may
// be off by 1 msat. So we reverse what the handler does to find the exact amount.
val fee = calculateFees(amountBeforeFee.toLong().msat)
return (finalAmount - fee).takeUnless { it < 0.msat }
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package fr.acinq.lightning

import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.utils.sat
import kotlin.test.Test
import kotlin.test.assertEquals

class TrampolineFeesTestsCommon {

@Test
fun `calculate fees`() {
val trampolineFees = TrampolineFees(
feeBase = 4.sat,
feeProportional = 4_000,
cltvExpiryDelta = CltvExpiryDelta(576)
)

assertEquals(4_000.msat, trampolineFees.calculateFees(0.msat))
assertEquals(4_000.msat, trampolineFees.calculateFees(1.msat))
assertEquals(4_004.msat, trampolineFees.calculateFees(1_001.msat))
assertEquals(8_000.msat, trampolineFees.calculateFees(1_000_000.msat))
assertEquals(8_003.msat, trampolineFees.calculateFees(1_000_789.msat))
assertEquals(497_827.msat, trampolineFees.calculateFees(123_456_789.msat))
}

@Test
fun `calculate reverse amount`() {
val trampolineFees = TrampolineFees(
feeBase = 4.sat,
feeProportional = 4_000,
cltvExpiryDelta = CltvExpiryDelta(576)
)

// amount available in the wallet -> amount that should be provided in the trampoline payload
val testCases = listOf(
0.msat to null,
1.msat to null,
1_000.msat to null,
4_000.msat to 0.msat,
4_001.msat to 1.msat,
4_004.msat to 4.msat,
8_000.msat to 3_985.msat,
8_016.msat to 4_000.msat,
1_000_000.msat to 992_032.msat,
100_000_000.msat to 99_597_610.msat,
123_456_789.msat to 122_960_946.msat
)

testCases.forEach { (availableAmount, expectedTrampolineAmount) ->
val trampolineAmount = trampolineFees.calculateReverseAmount(availableAmount)
assertEquals(expectedTrampolineAmount, trampolineAmount)
if (trampolineAmount != null) {
assertEquals(availableAmount, trampolineFees.calculateFees(trampolineAmount) + trampolineAmount)
}
}
}
}
Loading