diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializer.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializer.java index c398f545..840f879a 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializer.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializer.java @@ -18,19 +18,22 @@ class ArcInitializer { private final Map edgeMapping; private final long quantization; private final int piecewiseLinearApproximations; + private final int feeRateFactor; public ArcInitializer( MinCostFlow minCostFlow, IntegerMapping integerMapping, Map edgeMapping, long quantization, - int piecewiseLinearApproximations + int piecewiseLinearApproximations, + int feeRateFactor ) { this.minCostFlow = minCostFlow; this.pubkeyToIntegerMapping = integerMapping; this.edgeMapping = edgeMapping; this.quantization = quantization; this.piecewiseLinearApproximations = piecewiseLinearApproximations; + this.feeRateFactor = feeRateFactor; } public void addArcs(Collection edgesWithLiquidityInformation) { @@ -59,12 +62,13 @@ class ArcInitializer { return; } long unitCost = quantize(maximumCapacity) / uncertainButPossibleLiquidity; + long feeRateAdditionSummand = feeRateFactor * edge.policy().feeRate(); for (int i = 1; i <= remainingPieces; i++) { int arcIndex = minCostFlow.addArcWithCapacityAndUnitCost( startNode, endNode, capacityPiece, - i * unitCost + i * unitCost + feeRateAdditionSummand ); edgeMapping.put(arcIndex, edge); } diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputation.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputation.java index cc25409f..f62a4402 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputation.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputation.java @@ -8,11 +8,13 @@ import org.springframework.stereotype.Component; import java.util.Map; +import static de.cotto.lndmanagej.pickhardtpayments.PickhardtPaymentsConfiguration.DEFAULT_FEE_RATE_FACTOR; + @Component public class FlowComputation { + private final EdgeComputation edgeComputation; private final long quantization; private final int piecewiseLinearApproximations; - private final EdgeComputation edgeComputation; public FlowComputation( EdgeComputation edgeComputation, @@ -25,12 +27,17 @@ public class FlowComputation { } public Flows getOptimalFlows(Pubkey source, Pubkey target, Coins amount) { + return getOptimalFlows(source, target, amount, DEFAULT_FEE_RATE_FACTOR); + } + + public Flows getOptimalFlows(Pubkey source, Pubkey target, Coins amount, int feeRateFactor) { MinCostFlowSolver minCostFlowSolver = new MinCostFlowSolver( edgeComputation.getEdges(), Map.of(source, amount), Map.of(target, amount), quantization, - piecewiseLinearApproximations + piecewiseLinearApproximations, + feeRateFactor ); return minCostFlowSolver.solve(); } diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MinCostFlowSolver.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MinCostFlowSolver.java index e6c8ff8a..7b285ce2 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MinCostFlowSolver.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MinCostFlowSolver.java @@ -36,7 +36,8 @@ class MinCostFlowSolver { Map sources, Map sinks, long quantization, - int piecewiseLinearApproximations + int piecewiseLinearApproximations, + int feeRateFactor ) { this.quantization = quantization; ArcInitializer arcInitializer = new ArcInitializer( @@ -44,7 +45,8 @@ class MinCostFlowSolver { integerMapping, edgeMapping, quantization, - piecewiseLinearApproximations + piecewiseLinearApproximations, + feeRateFactor ); arcInitializer.addArcs(edgesWithLiquidityInformation); setSupply(sources, sinks); diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSplitter.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSplitter.java index d20e42a0..24ca8c24 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSplitter.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSplitter.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Component; import java.util.Set; +import static de.cotto.lndmanagej.pickhardtpayments.PickhardtPaymentsConfiguration.DEFAULT_FEE_RATE_FACTOR; import static java.util.stream.Collectors.toSet; @Component @@ -35,8 +36,17 @@ public class MultiPathPaymentSplitter { return getMultiPathPayment(source, target, amount); } + public MultiPathPayment getMultiPathPaymentTo(Pubkey target, Coins amount, int feeRateFactor) { + Pubkey source = grpcGetInfo.getPubkey(); + return getMultiPathPayment(source, target, amount, feeRateFactor); + } + public MultiPathPayment getMultiPathPayment(Pubkey source, Pubkey target, Coins amount) { - Flows flows = flowComputation.getOptimalFlows(source, target, amount); + return getMultiPathPayment(source, target, amount, DEFAULT_FEE_RATE_FACTOR); + } + + public MultiPathPayment getMultiPathPayment(Pubkey source, Pubkey target, Coins amount, int feeRateFactor) { + Flows flows = flowComputation.getOptimalFlows(source, target, amount, feeRateFactor); if (flows.isEmpty()) { return MultiPathPayment.FAILURE; } diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/PickhardtPaymentsConfiguration.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/PickhardtPaymentsConfiguration.java new file mode 100644 index 00000000..0dc2ec48 --- /dev/null +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/PickhardtPaymentsConfiguration.java @@ -0,0 +1,9 @@ +package de.cotto.lndmanagej.pickhardtpayments; + +public final class PickhardtPaymentsConfiguration { + public static final int DEFAULT_FEE_RATE_FACTOR = 0; + + private PickhardtPaymentsConfiguration() { + // do not instantiate me + } +} diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializerTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializerTest.java index 86c6e424..db437ced 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializerTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializerTest.java @@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; class ArcInitializerTest { private static final int QUANTIZATION = 1; private static final int PIECEWISE_LINEAR_APPROXIMATIONS = 1; + private static final int FEE_RATE_FACTOR = 0; static { Loader.loadNativeLibraries(); @@ -38,7 +39,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, QUANTIZATION, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ); @Test @@ -61,7 +63,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, quantization, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ); EdgeWithLiquidityInformation edgeWithLiquidityInformation = edge(EDGE, Coins.ofSatoshis(quantization)); @@ -113,7 +116,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, QUANTIZATION, - 2 + 2, + FEE_RATE_FACTOR ); arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); assertThat(minCostFlow.getUnitCost(1)).isEqualTo(1); @@ -127,7 +131,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, QUANTIZATION, - 5 + 5, + FEE_RATE_FACTOR ); arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); assertThat(minCostFlow.getNumArcs()).isEqualTo(5); @@ -140,7 +145,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, edgeWithLiquidityInformation.availableLiquidityLowerBound().satoshis(), - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ); arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); assertThat(minCostFlow.getUnitCost(0)).isEqualTo(0); @@ -153,7 +159,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, 20, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ); arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); assertThat(minCostFlow.getCapacity(0)).isEqualTo(1); @@ -166,7 +173,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, 10, - 2 + 2, + FEE_RATE_FACTOR ); arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); // one arc for the known liquidity (25 / 10 = 2), 100 / 10 - 2 = 8 remaining @@ -180,7 +188,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, 20, - 6 + 6, + FEE_RATE_FACTOR ); arcInitializer.addArcs(Set.of(edgeWithLiquidityInformation)); // one arc for the known liquidity (25 / 20 = 1), 100 / 20 - 1 = 4 remaining: 4 < 5, no additional arc added @@ -196,7 +205,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, QUANTIZATION, - piecesPerChannel + piecesPerChannel, + FEE_RATE_FACTOR ); arcInitializer.addArcs(List.of( edge(EDGE, Coins.ofSatoshis(100)), @@ -214,7 +224,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, quantization, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ); EdgeWithLiquidityInformation edgeWithLiquidityInformation = edge(EDGE, Coins.ofSatoshis(quantization - 1)); @@ -230,7 +241,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, quantization, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ); arcInitializer.addArcs(Set.of(edge(EDGE, Coins.ofSatoshis(20_123)))); assertThat(minCostFlow.getCapacity(0)).isEqualTo(201); @@ -244,7 +256,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, quantization, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ); arcInitializer.addArcs(List.of( edge(EDGE, Coins.ofSatoshis(20_123)), @@ -268,7 +281,8 @@ class ArcInitializerTest { integerMapping, edgeMapping, QUANTIZATION, - piecewiseLinearApproximations + piecewiseLinearApproximations, + FEE_RATE_FACTOR ); arcInitializer.addArcs(List.of( edge(EDGE, Coins.ofSatoshis(10_000)), @@ -340,6 +354,22 @@ class ArcInitializerTest { assertThat(minCostFlow.getUnitCost(0)).isEqualTo(10L); } + @Test + void computes_unit_cost_with_fee_rate() { + int feeRateFactor = 1; + ArcInitializer arcInitializer = new ArcInitializer( + minCostFlow, + integerMapping, + edgeMapping, + QUANTIZATION, + PIECEWISE_LINEAR_APPROXIMATIONS, + feeRateFactor + ); + EdgeWithLiquidityInformation edge = edge(EDGE, Coins.ofSatoshis(2_000_000)); + arcInitializer.addArcs(List.of(edge)); + assertThat(minCostFlow.getUnitCost(0)).isEqualTo(201); + } + private EdgeWithLiquidityInformation edge(Edge edge, Coins capacity) { Edge edgeWithCapacity = edge.withCapacity(capacity); return EdgeWithLiquidityInformation.forUpperBound(edgeWithCapacity, capacity); diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MinCostFlowSolverTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MinCostFlowSolverTest.java index f2cf8e47..b98d3a98 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MinCostFlowSolverTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MinCostFlowSolverTest.java @@ -28,6 +28,7 @@ class MinCostFlowSolverTest { private static final long QUANTIZATION = 1; private static final int PIECEWISE_LINEAR_APPROXIMATIONS = 1; + private static final int FEE_RATE_FACTOR = 0; private static final Coins ONE_SAT = Coins.ofSatoshis(PIECEWISE_LINEAR_APPROXIMATIONS); private static final Coins TWO_SATS = ONE_SAT.add(ONE_SAT); private static final Coins MANY_SATS = Coins.ofSatoshis(100_000_000); @@ -48,7 +49,8 @@ class MinCostFlowSolverTest { sources, sinks, QUANTIZATION, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ) ).doesNotThrowAnyException(); } @@ -65,7 +67,8 @@ class MinCostFlowSolverTest { sources, sinks, QUANTIZATION, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ) ).doesNotThrowAnyException(); } @@ -82,7 +85,8 @@ class MinCostFlowSolverTest { sources, sinks, QUANTIZATION, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ) ); } @@ -99,7 +103,8 @@ class MinCostFlowSolverTest { sources, sinks, QUANTIZATION, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ) ); } @@ -116,7 +121,8 @@ class MinCostFlowSolverTest { sources, sinks, QUANTIZATION, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ) ); } @@ -159,7 +165,8 @@ class MinCostFlowSolverTest { sources, sinks, quantization, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ).solve(); assertThat(flows).isEqualTo(new Flows(new Flow(EDGE_2_3, amount))); @@ -179,7 +186,8 @@ class MinCostFlowSolverTest { sources, sinks, quantization, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ).solve(); assertThat(flows).isEqualTo(new Flows(new Flow(EDGE_3_4, Coins.ofSatoshis(120_000)))); @@ -198,7 +206,8 @@ class MinCostFlowSolverTest { sources, sinks, quantization, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ).solve(); assertThat(flows).isEqualTo(new Flows()); @@ -218,7 +227,8 @@ class MinCostFlowSolverTest { sources, sinks, QUANTIZATION, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ); assertThat(minCostFlowSolver.solve()).isEqualTo(new Flows(FLOW_1_2, FLOW_3_4)); @@ -237,7 +247,8 @@ class MinCostFlowSolverTest { sources, sinks, QUANTIZATION, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ); assertThat(minCostFlowSolver.solve()).isEqualTo(new Flows(FLOW_1_2, FLOW_1_3)); @@ -281,7 +292,8 @@ class MinCostFlowSolverTest { sources, sinks, QUANTIZATION, - PIECEWISE_LINEAR_APPROXIMATIONS + PIECEWISE_LINEAR_APPROXIMATIONS, + FEE_RATE_FACTOR ).solve(); } } diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSplitterTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSplitterTest.java index f954db6a..cd77e8e2 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSplitterTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSplitterTest.java @@ -26,11 +26,13 @@ import static de.cotto.lndmanagej.model.PolicyFixtures.POLICY_1; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_4; +import static de.cotto.lndmanagej.pickhardtpayments.PickhardtPaymentsConfiguration.DEFAULT_FEE_RATE_FACTOR; import static de.cotto.lndmanagej.pickhardtpayments.model.EdgeFixtures.EDGE; import static de.cotto.lndmanagej.pickhardtpayments.model.FlowFixtures.FLOW; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -52,7 +54,7 @@ class MultiPathPaymentSplitterTest { @BeforeEach void setUp() { - when(flowComputation.getOptimalFlows(any(), any(), any())).thenReturn(new Flows()); + when(flowComputation.getOptimalFlows(any(), any(), any(), anyInt())).thenReturn(new Flows()); lenient().when(edgeComputation.getEdgeWithLiquidityInformation(EDGE)).thenReturn(noInformationFor(EDGE)); } @@ -60,18 +62,23 @@ class MultiPathPaymentSplitterTest { void getMultiPathPaymentTo_uses_own_pubkey_as_source() { when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4); multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY_2, AMOUNT); - verify(flowComputation).getOptimalFlows(PUBKEY_4, PUBKEY_2, AMOUNT); + verify(flowComputation).getOptimalFlows(PUBKEY_4, PUBKEY_2, AMOUNT, DEFAULT_FEE_RATE_FACTOR); } @Test - void getMultiPathPayment_failure() { - MultiPathPayment multiPathPayment = multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, AMOUNT); - assertThat(multiPathPayment.probability()).isZero(); + void getMultiPathPaymentTo_with_fee_rate() { + int feeRateFactor = 123; + when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4); + MultiPathPayment multiPathPayment = + multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY_2, AMOUNT, feeRateFactor); + assertThat(multiPathPayment.amount()).isEqualTo(Coins.NONE); + verify(flowComputation).getOptimalFlows(PUBKEY_4, PUBKEY_2, AMOUNT, feeRateFactor); } @Test void getMultiPathPaymentTo() { - when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, AMOUNT)).thenReturn(new Flows(FLOW)); + when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, AMOUNT, DEFAULT_FEE_RATE_FACTOR)) + .thenReturn(new Flows(FLOW)); when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY); MultiPathPayment multiPathPayment = multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY_2, AMOUNT); @@ -81,12 +88,19 @@ class MultiPathPaymentSplitterTest { assertThat(multiPathPayment).isEqualTo(expected); } + @Test + void getMultiPathPayment_failure() { + MultiPathPayment multiPathPayment = multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, AMOUNT); + assertThat(multiPathPayment.probability()).isZero(); + } + @Test void getMultiPathPayment_one_flow_probability() { long capacitySat = EDGE.capacity().satoshis(); Coins halfOfCapacity = Coins.ofSatoshis(capacitySat / 2); Flow flow = new Flow(EDGE, halfOfCapacity); - when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, halfOfCapacity)).thenReturn(new Flows(flow)); + when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, halfOfCapacity, DEFAULT_FEE_RATE_FACTOR)) + .thenReturn(new Flows(flow)); MultiPathPayment multiPathPayment = multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, halfOfCapacity); @@ -95,6 +109,15 @@ class MultiPathPaymentSplitterTest { .isEqualTo((1.0 * halfOfCapacity.satoshis() + 1) / (capacitySat + 1)); } + @Test + void getMultiPathPayment_with_fee_rate_factor() { + int feeRateFactor = 991; + MultiPathPayment multiPathPayment = + multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, AMOUNT, feeRateFactor); + assertThat(multiPathPayment.amount()).isEqualTo(Coins.NONE); + verify(flowComputation).getOptimalFlows(PUBKEY, PUBKEY_2, AMOUNT, feeRateFactor); + } + @Test void getMultiPathPayment_two_flows_probability() { long capacitySat = CAPACITY.satoshis(); @@ -106,7 +129,8 @@ class MultiPathPaymentSplitterTest { when(edgeComputation.getEdgeWithLiquidityInformation(edge1)).thenReturn(noInformationFor(edge1)); when(edgeComputation.getEdgeWithLiquidityInformation(edge2)).thenReturn(noInformationFor(edge2)); Coins totalAmount = halfOfCapacity.add(CAPACITY_2); - when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, totalAmount)).thenReturn(new Flows(flow1, flow2)); + when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, totalAmount, DEFAULT_FEE_RATE_FACTOR)) + .thenReturn(new Flows(flow1, flow2)); MultiPathPayment multiPathPayment = multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, totalAmount); @@ -122,7 +146,8 @@ class MultiPathPaymentSplitterTest { Coins halfOfCapacity = Coins.ofSatoshis(capacitySat / 2); Flow flow1 = new Flow(EDGE, halfOfCapacity); Flow flow2 = new Flow(EDGE, halfOfCapacity); - when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, EDGE.capacity())).thenReturn(new Flows(flow1, flow2)); + when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, EDGE.capacity(), DEFAULT_FEE_RATE_FACTOR)) + .thenReturn(new Flows(flow1, flow2)); MultiPathPayment multiPathPayment = multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, EDGE.capacity()); @@ -132,7 +157,8 @@ class MultiPathPaymentSplitterTest { @Test void getMultiPathPayment_adds_remainder_to_most_probable_route() { - when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, AMOUNT)).thenReturn(new Flows(FLOW)); + when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, AMOUNT, DEFAULT_FEE_RATE_FACTOR)) + .thenReturn(new Flows(FLOW)); assumeThat(FLOW.amount()).isLessThan(AMOUNT); MultiPathPayment multiPathPayment = multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, AMOUNT); assertThat(multiPathPayment.routes().iterator().next().amount()).isEqualTo(AMOUNT); @@ -145,7 +171,8 @@ class MultiPathPaymentSplitterTest { EdgeWithLiquidityInformation withLiquidityInformation = EdgeWithLiquidityInformation.forKnownLiquidity(edge, Coins.ofSatoshis(1)); when(edgeComputation.getEdgeWithLiquidityInformation(edge)).thenReturn(withLiquidityInformation); - when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, amount)).thenReturn(new Flows(FLOW)); + when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, amount, DEFAULT_FEE_RATE_FACTOR)) + .thenReturn(new Flows(FLOW)); Route expected = new Route(List.of(edge), amount).withLiquidityInformation(Set.of(withLiquidityInformation)); MultiPathPayment multiPathPayment = multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, amount); @@ -167,10 +194,11 @@ class MultiPathPaymentSplitterTest { when(edgeComputation.getEdgeWithLiquidityInformation(edgeLargeCapacity)).thenReturn(liquidityInformationLarge); when(edgeComputation.getEdgeWithLiquidityInformation(edgeSmallCapacity)).thenReturn(liquidityInformationSmall); - when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, AMOUNT)).thenReturn(new Flows( - new Flow(edgeLargeCapacity, oneSat), - new Flow(edgeSmallCapacity, oneSat) - )); + when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, AMOUNT, DEFAULT_FEE_RATE_FACTOR)) + .thenReturn(new Flows( + new Flow(edgeLargeCapacity, oneSat), + new Flow(edgeSmallCapacity, oneSat) + )); assumeThat(FLOW.amount()).isLessThan(AMOUNT); MultiPathPayment multiPathPayment = multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, AMOUNT); Route route1 = new Route(List.of(edgeLargeCapacity), oneSat) diff --git a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerIT.java b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerIT.java index ca236481..268e1e45 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerIT.java @@ -14,6 +14,7 @@ import org.springframework.test.web.servlet.MockMvc; import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; +import static de.cotto.lndmanagej.pickhardtpayments.PickhardtPaymentsConfiguration.DEFAULT_FEE_RATE_FACTOR; import static de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPaymentFixtures.MULTI_PATH_PAYMENT; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasSize; @@ -21,6 +22,7 @@ import static org.hamcrest.core.Is.is; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SuppressWarnings("CPD-START") @Import(ObjectMapperConfiguration.class) @@ -43,9 +45,9 @@ class PickhardtPaymentsControllerIT { String amountAsString = String.valueOf(amount.satoshis()); String feesAsString = String.valueOf(MULTI_PATH_PAYMENT.fees().milliSatoshis()); double expectedProbability = MULTI_PATH_PAYMENT.probability(); - when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, amount)) + when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, amount, DEFAULT_FEE_RATE_FACTOR)) .thenReturn(MULTI_PATH_PAYMENT); - mockMvc.perform(get(PREFIX + "/to/" + PUBKEY + "/amount/" + amount.satoshis())) + mockMvc.perform(get("%s/to/%s/amount/%d".formatted(PREFIX, PUBKEY, amount.satoshis()))) .andExpect(jsonPath("$.probability", is(expectedProbability))) .andExpect(jsonPath("$.amountSat", is(amountAsString))) .andExpect(jsonPath("$.feesMilliSat", is(feesAsString))) @@ -58,6 +60,17 @@ class PickhardtPaymentsControllerIT { .andExpect(jsonPath("$.routes[0].feeRate", is(200))); } + @Test + void sendTo_with_fee_rate_factor() throws Exception { + int feeRateFactor = 999; + Coins amount = MULTI_PATH_PAYMENT.amount(); + when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, amount, feeRateFactor)) + .thenReturn(MULTI_PATH_PAYMENT); + String url = "%s/to/%s/amount/%d/fee-rate-factor/%d" + .formatted(PREFIX, PUBKEY, amount.satoshis(), feeRateFactor); + mockMvc.perform(get(url)).andExpect(status().isOk()); + } + @Test void send() throws Exception { Coins amount = MULTI_PATH_PAYMENT.amount(); @@ -66,9 +79,13 @@ class PickhardtPaymentsControllerIT { double expectedProbability = MULTI_PATH_PAYMENT.probability(); when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, amount)) .thenReturn(MULTI_PATH_PAYMENT); - when(multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, Coins.ofSatoshis(1_234))) - .thenReturn(MULTI_PATH_PAYMENT); - mockMvc.perform(get(PREFIX + "/from/" + PUBKEY + "/to/" + PUBKEY_2 + "/amount/" + 1_234)) + when(multiPathPaymentSplitter.getMultiPathPayment( + PUBKEY, + PUBKEY_2, + Coins.ofSatoshis(1_234), + DEFAULT_FEE_RATE_FACTOR + )).thenReturn(MULTI_PATH_PAYMENT); + mockMvc.perform(get("%s/from/%s/to/%s/amount/%d".formatted(PREFIX, PUBKEY, PUBKEY_2, 1_234))) .andExpect(jsonPath("$.probability", is(expectedProbability))) .andExpect(jsonPath("$.amountSat", is(amountAsString))) .andExpect(jsonPath("$.feesMilliSat", is(feesAsString))) @@ -80,4 +97,15 @@ class PickhardtPaymentsControllerIT { .andExpect(jsonPath("$.routes[0].feesMilliSat", is(feesAsString))) .andExpect(jsonPath("$.routes[0].feeRate", is(200))); } + + @Test + void send_with_fee_rate_factor() throws Exception { + int feeRateFactor = 999; + Coins amount = MULTI_PATH_PAYMENT.amount(); + when(multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, amount, feeRateFactor)) + .thenReturn(MULTI_PATH_PAYMENT); + String url = "%s/from/%s/to/%s/amount/%d/fee-rate-factor/%d" + .formatted(PREFIX, PUBKEY, PUBKEY_2, amount.satoshis(), feeRateFactor); + mockMvc.perform(get(url)).andExpect(status().isOk()); + } } diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/PickhardtPaymentsController.java b/web/src/main/java/de/cotto/lndmanagej/controller/PickhardtPaymentsController.java index 1856091a..2804c386 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/PickhardtPaymentsController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/PickhardtPaymentsController.java @@ -5,11 +5,14 @@ import de.cotto.lndmanagej.controller.dto.MultiPathPaymentDto; import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.pickhardtpayments.MultiPathPaymentSplitter; +import de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPayment; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import static de.cotto.lndmanagej.pickhardtpayments.PickhardtPaymentsConfiguration.DEFAULT_FEE_RATE_FACTOR; + @RestController @RequestMapping("/beta/pickhardt-payments/") public class PickhardtPaymentsController { @@ -19,14 +22,40 @@ public class PickhardtPaymentsController { this.multiPathPaymentSplitter = multiPathPaymentSplitter; } + @Timed + @GetMapping("/to/{pubkey}/amount/{amount}/fee-rate-factor/{feeRateFactor}") + public MultiPathPaymentDto sendTo( + @PathVariable Pubkey pubkey, + @PathVariable long amount, + @PathVariable int feeRateFactor + ) { + Coins coins = Coins.ofSatoshis(amount); + MultiPathPayment multiPathPaymentTo = + multiPathPaymentSplitter.getMultiPathPaymentTo(pubkey, coins, feeRateFactor); + return MultiPathPaymentDto.fromModel(multiPathPaymentTo); + } + @Timed @GetMapping("/to/{pubkey}/amount/{amount}") public MultiPathPaymentDto sendTo( @PathVariable Pubkey pubkey, @PathVariable long amount + ) { + return sendTo(pubkey, amount, DEFAULT_FEE_RATE_FACTOR); + } + + @Timed + @GetMapping("/from/{source}/to/{target}/amount/{amount}/fee-rate-factor/{feeRateFactor}") + public MultiPathPaymentDto send( + @PathVariable Pubkey source, + @PathVariable Pubkey target, + @PathVariable long amount, + @PathVariable int feeRateFactor ) { Coins coins = Coins.ofSatoshis(amount); - return MultiPathPaymentDto.fromModel(multiPathPaymentSplitter.getMultiPathPaymentTo(pubkey, coins)); + MultiPathPayment multiPathPayment = + multiPathPaymentSplitter.getMultiPathPayment(source, target, coins, feeRateFactor); + return MultiPathPaymentDto.fromModel(multiPathPayment); } @Timed @@ -36,7 +65,6 @@ public class PickhardtPaymentsController { @PathVariable Pubkey target, @PathVariable long amount ) { - Coins coins = Coins.ofSatoshis(amount); - return MultiPathPaymentDto.fromModel(multiPathPaymentSplitter.getMultiPathPayment(source, target, coins)); + return send(source, target, amount, DEFAULT_FEE_RATE_FACTOR); } } diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerTest.java index cbe93721..b52765ec 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerTest.java @@ -11,6 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; +import static de.cotto.lndmanagej.pickhardtpayments.PickhardtPaymentsConfiguration.DEFAULT_FEE_RATE_FACTOR; import static de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPaymentFixtures.MULTI_PATH_PAYMENT; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -26,17 +27,43 @@ class PickhardtPaymentsControllerTest { @Test void sendTo() { - when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, Coins.ofSatoshis(456))) + when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, Coins.ofSatoshis(456), DEFAULT_FEE_RATE_FACTOR)) .thenReturn(MULTI_PATH_PAYMENT); assertThat(controller.sendTo(PUBKEY, 456)) .isEqualTo(MultiPathPaymentDto.fromModel(MULTI_PATH_PAYMENT)); } @Test - void send() { - when(multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, Coins.ofSatoshis(123))) + void sendTo_with_fee_rate_factor() { + int feeRateFactor = 10; + when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, Coins.ofSatoshis(456), feeRateFactor)) .thenReturn(MULTI_PATH_PAYMENT); + assertThat(controller.sendTo(PUBKEY, 456, feeRateFactor)) + .isEqualTo(MultiPathPaymentDto.fromModel(MULTI_PATH_PAYMENT)); + } + + @Test + void send() { + when(multiPathPaymentSplitter.getMultiPathPayment( + PUBKEY, + PUBKEY_2, + Coins.ofSatoshis(123), + DEFAULT_FEE_RATE_FACTOR + )).thenReturn(MULTI_PATH_PAYMENT); assertThat(controller.send(PUBKEY, PUBKEY_2, 123)) .isEqualTo(MultiPathPaymentDto.fromModel(MULTI_PATH_PAYMENT)); } + + @Test + void send_with_fee_rate_factor() { + int feeRateFactor = 20; + when(multiPathPaymentSplitter.getMultiPathPayment( + PUBKEY, + PUBKEY_2, + Coins.ofSatoshis(123), + feeRateFactor + )).thenReturn(MULTI_PATH_PAYMENT); + assertThat(controller.send(PUBKEY, PUBKEY_2, 123, feeRateFactor)) + .isEqualTo(MultiPathPaymentDto.fromModel(MULTI_PATH_PAYMENT)); + } }