diff --git a/model/src/main/java/de/cotto/lndmanagej/model/Route.java b/model/src/main/java/de/cotto/lndmanagej/model/Route.java index 0ce4070d..233e8dbb 100644 --- a/model/src/main/java/de/cotto/lndmanagej/model/Route.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/Route.java @@ -1,5 +1,6 @@ package de.cotto.lndmanagej.model; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -43,7 +44,9 @@ public Coins getFeesWithFirstHop() { return Coins.NONE; } Coins forwardAmountForFirstHop = getFees().add(amount); - return getFees().add(getFeesForEdgeAndAmount(forwardAmountForFirstHop, edgesWithLiquidityInformation.get(0))); + return getFees().add( + getFeesForEdgeAndAmount(forwardAmountForFirstHop, null, edgesWithLiquidityInformation.get(0)) + ); } public double getProbability() { @@ -139,7 +142,9 @@ private static List computeFees(List edges, List feesForHops = new ArrayList<>(); feesForHops.add(Coins.NONE); for (int i = edges.size() - 1; i > 0; i--) { - Coins feesForHop = getFeesForEdgeAndAmount(amountWithFees, edges.get(i)); + EdgeWithLiquidityInformation edge = edges.get(i); + EdgeWithLiquidityInformation previousEdge = edges.get(i - 1); + Coins feesForHop = getFeesForEdgeAndAmount(amountWithFees, previousEdge, edge); amountWithFees = amountWithFees.add(feesForHop); fees = fees.add(feesForHop); feesForHops.add(0, feesForHop); @@ -147,10 +152,37 @@ private static List computeFees(List edges, return feesForHops; } - private static Coins getFeesForEdgeAndAmount(Coins amountWithFees, EdgeWithLiquidityInformation edge) { + private static Coins getFeesForEdgeAndAmount( + Coins amountWithFees, + @Nullable EdgeWithLiquidityInformation previousEdge, + EdgeWithLiquidityInformation edge + ) { long feeRate = edge.policy().feeRate(); + Coins relativeFees = getRelativeFees(amountWithFees, feeRate); Coins baseFeeForHop = edge.policy().baseFee(); - Coins relativeFees = Coins.ofMilliSatoshis(feeRate * amountWithFees.milliSatoshis() / 1_000_000); - return baseFeeForHop.add(relativeFees); + Coins outboundFees = baseFeeForHop.add(relativeFees); + + Coins inboundFees = getInboundFees(amountWithFees.add(outboundFees), previousEdge); + Coins combinedFees = outboundFees.add(inboundFees); + if (combinedFees.isNegative()) { + return Coins.NONE; + } + return combinedFees; + } + + private static Coins getInboundFees(Coins amountWithFees, @Nullable EdgeWithLiquidityInformation previousEdge) { + if (previousEdge == null) { + return Coins.NONE; + } + Policy policyForInboundFees = previousEdge.edge().reversePolicy(); + + Coins inboundBaseFeeForHop = policyForInboundFees.inboundBaseFee(); + long inboundFeeRate = policyForInboundFees.inboundFeeRate(); + Coins inboundRelativeFees = getRelativeFees(amountWithFees, inboundFeeRate); + return inboundBaseFeeForHop.add(inboundRelativeFees); + } + + private static Coins getRelativeFees(Coins amount, long feeRate) { + return Coins.ofMilliSatoshis(feeRate * amount.milliSatoshis() / 1_000_000); } } diff --git a/model/src/test/java/de/cotto/lndmanagej/model/RouteTest.java b/model/src/test/java/de/cotto/lndmanagej/model/RouteTest.java index 18357c18..3fb896c1 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/RouteTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/RouteTest.java @@ -164,8 +164,7 @@ void fees_amount_with_milli_sat() { Coins baseFee1 = Coins.ofMilliSatoshis(15); Coins baseFee2 = Coins.ofMilliSatoshis(10); Coins expectedFees = - Coins.ofMilliSatoshis((long) (amount.milliSatoshis() * 1.0 * ppm2 / ONE_MILLION)) - .add(baseFee2); + totalFeesFor(amount, ppm2, baseFee2); Policy policy1 = policy(baseFee1, ppm1); Edge hop1 = new Edge(CHANNEL_ID, PUBKEY, PUBKEY_2, CAPACITY, policy1, Policy.UNKNOWN); Policy policy2 = policy(baseFee2, ppm2); @@ -195,8 +194,7 @@ void fees_two_hops() { int ppm1 = 100; int ppm2 = 200; Coins expectedFees2 = - Coins.ofMilliSatoshis((long) (amount.milliSatoshis() * 1.0 * ppm2 / ONE_MILLION)) - .add(baseFee2); + totalFeesFor(amount, ppm2, baseFee2); Coins expectedFees1 = Coins.NONE; Coins expectedFees = expectedFees1.add(expectedFees2); BasicRoute basicRoute = new BasicRoute(List.of( @@ -218,8 +216,7 @@ void fees_three_hops() { int ppm3 = 300; Coins expectedFees3 = Coins.NONE; Coins expectedFees2 = - Coins.ofMilliSatoshis((long) (amount.milliSatoshis() * 1.0 * ppm3 / ONE_MILLION)) - .add(baseFee3); + totalFeesFor(amount, ppm3, baseFee3); long amountWithFeesLastHop = amount.add(expectedFees2).milliSatoshis(); Coins expectedFees1 = Coins.ofMilliSatoshis( (long) (amountWithFeesLastHop * 1.0 * ppm2 / ONE_MILLION) @@ -234,6 +231,71 @@ void fees_three_hops() { assertThat(route.getFees()).isEqualTo(expectedFees); } + @Test + void fees_two_hops_with_negative_inbound_fees() { + Coins amount = Coins.ofSatoshis(2_500_000); + Coins baseFee1 = Coins.ofMilliSatoshis(20); + Coins inboundBaseFee1 = Coins.ofMilliSatoshis(-3); + Coins baseFee2 = Coins.ofMilliSatoshis(6); + int ppm1 = 100; + int inboundPpm1 = -30; + int ppm2 = 200; + Coins expectedOutboundFees = totalFeesFor(amount, ppm2, baseFee2); + Coins expectedInboundFees = totalFeesFor(amount.add(expectedOutboundFees), inboundPpm1, inboundBaseFee1); + Coins expectedFees = expectedOutboundFees.add(expectedInboundFees); + BasicRoute basicRoute = new BasicRoute(List.of( + new Edge( + CHANNEL_ID, + PUBKEY, + PUBKEY_3, + CAPACITY, + policy(baseFee1, ppm1), + inboundPolicy(inboundBaseFee1, inboundPpm1) + ), + new Edge( + CHANNEL_ID_2, + PUBKEY_3, + PUBKEY_4, + CAPACITY, + policy(baseFee2, ppm2), + inboundPolicy(Coins.NONE, 0) + ) + ), amount); + Route route = new Route(basicRoute); + assertThat(route.getFees()).isEqualTo(expectedFees); + } + + @Test + void fees_two_hops_with_very_negative_inbound_fees() { + Coins amount = Coins.ofSatoshis(1_500_000); + Coins baseFee1 = Coins.ofMilliSatoshis(10); + Coins inboundBaseFee1 = Coins.ofMilliSatoshis(-15); + Coins baseFee2 = Coins.ofMilliSatoshis(6); + int ppm1 = 100; + int inboundPpm1 = -300; + int ppm2 = 200; + BasicRoute basicRoute = new BasicRoute(List.of( + new Edge( + CHANNEL_ID, + PUBKEY, + PUBKEY_2, + CAPACITY, + policy(baseFee1, ppm1), + inboundPolicy(inboundBaseFee1, inboundPpm1) + ), + new Edge( + CHANNEL_ID_2, + PUBKEY, + PUBKEY_2, + CAPACITY, + policy(baseFee2, ppm2), + inboundPolicy(Coins.NONE, 0) + ) + ), amount); + Route route = new Route(basicRoute); + assertThat(route.getFees()).isEqualTo(Coins.NONE); + } + @Test void feesWithFirstHop_empty() { BasicRoute basicRoute = new BasicRoute(List.of(), Coins.ofSatoshis(1_500_000)); @@ -504,4 +566,12 @@ private List edgesWithTimeLockDeltas(int... timeLockDeltas) { private Policy policy(Coins baseFee, int ppm) { return new Policy(ppm, baseFee, true, TIME_LOCK_DELTA, MIN_HTLC, MAX_HTLC); } + + private Policy inboundPolicy(Coins inboundBaseFee, int inboundPpm) { + return new Policy(0, Coins.NONE, inboundPpm, inboundBaseFee, true, TIME_LOCK_DELTA, MIN_HTLC, MAX_HTLC); + } + + private Coins totalFeesFor(Coins amount, int ppm, Coins baseFee) { + return Coins.ofMilliSatoshis((long) (amount.milliSatoshis() * 1.0 * ppm / ONE_MILLION)).add(baseFee); + } }