diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/EdgeComputation.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/EdgeComputation.java index 3b9997dd..b0530f7c 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/EdgeComputation.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/EdgeComputation.java @@ -9,8 +9,10 @@ import de.cotto.lndmanagej.model.DirectedChannelEdge; import de.cotto.lndmanagej.model.Edge; import de.cotto.lndmanagej.model.EdgeWithLiquidityInformation; import de.cotto.lndmanagej.model.LocalChannel; +import de.cotto.lndmanagej.model.Policy; import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.pickhardtpayments.model.EdgesWithLiquidityInformation; +import de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions; import de.cotto.lndmanagej.service.BalanceService; import de.cotto.lndmanagej.service.ChannelService; import de.cotto.lndmanagej.service.LiquidityBoundsService; @@ -27,8 +29,6 @@ import java.util.function.Function; @Component public class EdgeComputation { - private static final long NO_FEE_RATE_LIMIT = -1L; - private final Logger logger = LoggerFactory.getLogger(getClass()); private final GrpcGraph grpcGraph; @@ -57,11 +57,7 @@ public class EdgeComputation { this.routeHintService = routeHintService; } - public EdgesWithLiquidityInformation getEdges() { - return getEdges(NO_FEE_RATE_LIMIT); - } - - public EdgesWithLiquidityInformation getEdges(long feeRateLimit) { + public EdgesWithLiquidityInformation getEdges(PaymentOptions paymentOptions) { Set channelEdges = grpcGraph.getChannelEdges().orElse(null); if (channelEdges == null) { logger.warn("Unable to get graph"); @@ -71,7 +67,7 @@ public class EdgeComputation { Pubkey ownPubkey = grpcGetInfo.getPubkey(); Set edgesFromPaymentHints = routeHintService.getEdgesFromPaymentHints(); for (DirectedChannelEdge channelEdge : Sets.union(channelEdges, edgesFromPaymentHints)) { - if (shouldIgnore(channelEdge, feeRateLimit)) { + if (shouldIgnore(channelEdge, paymentOptions, ownPubkey)) { continue; } ChannelId channelId = channelEdge.channelId(); @@ -111,14 +107,31 @@ public class EdgeComputation { return withFeeReserve.subtract(Coins.ofSatoshis(1_000)).maximum(Coins.NONE); } - private boolean shouldIgnore(DirectedChannelEdge channelEdge, long feeRateLimit) { - if (channelEdge.policy().disabled()) { + private boolean shouldIgnore(DirectedChannelEdge channelEdge, PaymentOptions paymentOptions, Pubkey pubkey) { + Policy policy = channelEdge.policy(); + if (policy.disabled()) { return true; } - if (feeRateLimit == NO_FEE_RATE_LIMIT) { + Long feeRateLimit = paymentOptions.feeRateLimit().orElse(null); + if (feeRateLimit == null) { return false; } - return channelEdge.policy().feeRate() >= feeRateLimit; + long feeRate = policy.feeRate(); + if (feeRate >= feeRateLimit) { + return true; + } + if (isIncomingEdge(channelEdge, pubkey)) { + return false; + } + Long feeRateLimitFirstHops = paymentOptions.feeRateLimitExceptIncomingHops().orElse(null); + if (feeRateLimitFirstHops == null) { + return false; + } + return feeRate >= feeRateLimitFirstHops; + } + + private boolean isIncomingEdge(DirectedChannelEdge channelEdge, Pubkey ownPubkey) { + return ownPubkey.equals(channelEdge.target()); } private Optional getKnownLiquidity(Edge edge, Pubkey ownPubKey) { 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 bca06c39..9432a2a1 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 @@ -38,7 +38,7 @@ public class FlowComputation { int quantization = getQuantization(amount); int piecewiseLinearApproximations = configurationService.getIntegerValue(PIECEWISE_LINEAR_APPROXIMATIONS) .orElse(DEFAULT_PIECEWISE_LINEAR_APPROXIMATIONS); - EdgesWithLiquidityInformation edges = getEdges(paymentOptions); + EdgesWithLiquidityInformation edges = edgeComputation.getEdges(paymentOptions); MinCostFlowSolver minCostFlowSolver = new MinCostFlowSolver( edges, Map.of(source, amount), @@ -52,16 +52,6 @@ public class FlowComputation { return minCostFlowSolver.solve(); } - private EdgesWithLiquidityInformation getEdges(PaymentOptions paymentOptions) { - EdgesWithLiquidityInformation edges; - if (paymentOptions.feeRateLimit().isPresent()) { - edges = edgeComputation.getEdges(paymentOptions.feeRateLimit().get()); - } else { - edges = edgeComputation.getEdges(); - } - return edges; - } - private int getQuantization(Coins amount) { int quantization = configurationService.getIntegerValue(QUANTIZATION) .orElse(DEFAULT_QUANTIZATION); diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/TopUpService.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/TopUpService.java index c68560b4..13e1c446 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/TopUpService.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/TopUpService.java @@ -88,7 +88,7 @@ public class TopUpService { String alias = nodeService.getAlias(pubkey); return PaymentStatus.createFailure("Unable to create payment request (%s, %s)".formatted(pubkey, alias)); } - PaymentOptions paymentOptions = PaymentOptions.forTopUp(ourFeeRate, pubkey); + PaymentOptions paymentOptions = PaymentOptions.forTopUp(ourFeeRate, peerFeeRate, pubkey); return multiPathPaymentSender.payPaymentRequest(paymentRequest, paymentOptions); } diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/model/PaymentOptions.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/model/PaymentOptions.java index 59c56664..9bb10bbe 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/model/PaymentOptions.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/model/PaymentOptions.java @@ -7,20 +7,27 @@ import java.util.Optional; public record PaymentOptions( int feeRateWeight, Optional feeRateLimit, + Optional feeRateLimitExceptIncomingHops, boolean ignoreFeesForOwnChannels, Optional peer ) { public static final PaymentOptions DEFAULT_PAYMENT_OPTIONS = forFeeRateWeight(0); public static PaymentOptions forFeeRateWeight(int feeRateWeight) { - return new PaymentOptions(feeRateWeight, Optional.empty(), true, Optional.empty()); + return new PaymentOptions(feeRateWeight, Optional.empty(), Optional.empty(), true, Optional.empty()); } public static PaymentOptions forFeeRateLimit(long feeRateLimit) { - return new PaymentOptions(0, Optional.of(feeRateLimit), true, Optional.empty()); + return new PaymentOptions(0, Optional.of(feeRateLimit), Optional.of(feeRateLimit), true, Optional.empty()); } - public static PaymentOptions forTopUp(long feeRateLimit, Pubkey peer) { - return new PaymentOptions(5, Optional.of(feeRateLimit), false, Optional.of(peer)); + public static PaymentOptions forTopUp(long ourFeeRate, long peerFeeRate, Pubkey peer) { + return new PaymentOptions( + 5, + Optional.of(ourFeeRate), + Optional.of(ourFeeRate - peerFeeRate), + false, + Optional.of(peer) + ); } } diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/EdgeComputationTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/EdgeComputationTest.java index ea5a54a8..ab7432e0 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/EdgeComputationTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/EdgeComputationTest.java @@ -9,6 +9,7 @@ import de.cotto.lndmanagej.model.EdgeWithLiquidityInformation; import de.cotto.lndmanagej.model.Node; import de.cotto.lndmanagej.model.Policy; import de.cotto.lndmanagej.model.Pubkey; +import de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions; import de.cotto.lndmanagej.service.BalanceService; import de.cotto.lndmanagej.service.ChannelService; import de.cotto.lndmanagej.service.LiquidityBoundsService; @@ -37,6 +38,7 @@ import static de.cotto.lndmanagej.model.PolicyFixtures.POLICY_WITH_BASE_FEE; 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.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; @@ -79,23 +81,24 @@ class EdgeComputationTest { @Test void no_graph() { - assertThat(edgeComputation.getEdges().edges()).isEmpty(); + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()).isEmpty(); } @Test void does_not_add_edge_for_disabled_channel() { DirectedChannelEdge edge = new DirectedChannelEdge(CHANNEL_ID, CAPACITY, PUBKEY, PUBKEY_2, POLICY_DISABLED); when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(edge))); - assertThat(edgeComputation.getEdges().edges()).isEmpty(); + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()).isEmpty(); } @Test void does_not_add_edge_with_fee_rate_at_or_above_limit() { int feeRateLimit = 199; - Policy policyExpensive = new Policy(200, Coins.NONE, true, 40, Coins.ofSatoshis(0)); + PaymentOptions paymentOptions = PaymentOptions.forFeeRateLimit(feeRateLimit); + Policy policyExpensive = policy(200); // needs to be excluded to avoid sending top-up payments in a tiny loop: S-X-S - Policy policyAtLimit = new Policy(199, Coins.NONE, true, 40, Coins.ofSatoshis(0)); - Policy policyOk = new Policy(198, Coins.NONE, true, 40, Coins.ofSatoshis(0)); + Policy policyAtLimit = policy(199); + Policy policyOk = policy(198); DirectedChannelEdge edgeExpensive = new DirectedChannelEdge(CHANNEL_ID, CAPACITY, PUBKEY, PUBKEY_2, policyExpensive); DirectedChannelEdge edgeAtLimit = @@ -103,8 +106,33 @@ class EdgeComputationTest { DirectedChannelEdge edgeOk = new DirectedChannelEdge(CHANNEL_ID_3, CAPACITY, PUBKEY, PUBKEY_2, policyOk); when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(edgeExpensive, edgeAtLimit, edgeOk))); - assertThat(edgeComputation.getEdges(feeRateLimit).edges().stream().map(EdgeWithLiquidityInformation::channelId)) - .containsExactly(CHANNEL_ID_3); + assertThat( + edgeComputation.getEdges(paymentOptions).edges().stream().map(EdgeWithLiquidityInformation::channelId) + ).containsExactly(CHANNEL_ID_3); + } + + @Test + void does_not_add_first_hop_edge_with_fee_rate_at_or_above_limit_for_first_hops() { + Pubkey ownPubkey = EDGE.startNode(); + Pubkey topUpPeer = PUBKEY_4; + int feeRateLimit = 200; + int feeRateLimitForFirstHops = 100; + + when(grpcGetInfo.getPubkey()).thenReturn(ownPubkey); + PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRateLimit, feeRateLimitForFirstHops, topUpPeer); + Policy lastHopPolicy = policy(199); + Policy firstHopPolicyExpensive = policy(100); + Policy firstHopPolicyOk = policy(99); + DirectedChannelEdge lastHop = + new DirectedChannelEdge(CHANNEL_ID, CAPACITY, topUpPeer, ownPubkey, lastHopPolicy); + DirectedChannelEdge firstHopExpensive = + new DirectedChannelEdge(CHANNEL_ID_2, CAPACITY, ownPubkey, PUBKEY_2, firstHopPolicyExpensive); + DirectedChannelEdge firstHopOk = + new DirectedChannelEdge(CHANNEL_ID_3, CAPACITY, ownPubkey, PUBKEY_2, firstHopPolicyOk); + when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(lastHop, firstHopExpensive, firstHopOk))); + assertThat( + edgeComputation.getEdges(paymentOptions).edges().stream().map(EdgeWithLiquidityInformation::channelId) + ).containsExactlyInAnyOrder(CHANNEL_ID, CHANNEL_ID_3); } @Test @@ -112,7 +140,7 @@ class EdgeComputationTest { DirectedChannelEdge edge = new DirectedChannelEdge(CHANNEL_ID, CAPACITY, PUBKEY, PUBKEY_2, POLICY_WITH_BASE_FEE); when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(edge))); - assertThat(edgeComputation.getEdges().edges()).isNotEmpty(); + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()).isNotEmpty(); } @Test @@ -124,7 +152,7 @@ class EdgeComputationTest { Coins availableKnownLiquidity = getAvailableKnownLiquidity(knownLiquidity); when(balanceService.getAvailableLocalBalance(EDGE.channelId())).thenReturn(knownLiquidity); - assertThat(edgeComputation.getEdges().edges()) + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()) .contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, availableKnownLiquidity)); } @@ -143,7 +171,7 @@ class EdgeComputationTest { edge.policy() ) )); - assertThat(edgeComputation.getEdges().edges()) + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()) .contains(EdgeWithLiquidityInformation.forKnownLiquidity(edge, fiftyCoins)); } @@ -152,7 +180,7 @@ class EdgeComputationTest { mockEdge(); when(grpcGetInfo.getPubkey()).thenReturn(EDGE.startNode()); - assertThat(edgeComputation.getEdges().edges()) + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()) .contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, Coins.NONE)); } @@ -162,7 +190,7 @@ class EdgeComputationTest { mockOfflinePeer(); when(grpcGetInfo.getPubkey()).thenReturn(EDGE.startNode()); - assertThat(edgeComputation.getEdges().edges()) + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()) .contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, Coins.NONE)); verify(balanceService, never()).getAvailableLocalBalance(EDGE.channelId()); } @@ -176,7 +204,7 @@ class EdgeComputationTest { Coins availableKnownLiquidity = getAvailableKnownLiquidity(knownLiquidity); when(balanceService.getAvailableRemoteBalance(EDGE.channelId())).thenReturn(knownLiquidity); - assertThat(edgeComputation.getEdges().edges()) + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()) .contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, availableKnownLiquidity)); } @@ -185,7 +213,7 @@ class EdgeComputationTest { mockEdge(); when(grpcGetInfo.getPubkey()).thenReturn(EDGE.endNode()); - assertThat(edgeComputation.getEdges().edges()) + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()) .contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, Coins.NONE)); } @@ -195,7 +223,7 @@ class EdgeComputationTest { mockOfflinePeer(); when(grpcGetInfo.getPubkey()).thenReturn(EDGE.endNode()); - assertThat(edgeComputation.getEdges().edges()) + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()) .contains(EdgeWithLiquidityInformation.forKnownLiquidity(EDGE, Coins.NONE)); verify(balanceService, never()).getAvailableLocalBalance(EDGE.channelId()); } @@ -211,14 +239,14 @@ class EdgeComputationTest { mockEdge(); Coins upperBound = Coins.ofSatoshis(100); mockUpperBound(upperBound); - assertThat(edgeComputation.getEdges().edges()) + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()) .contains(EdgeWithLiquidityInformation.forUpperBound(EDGE, upperBound)); } @Test void default_if_no_liquidity_information_is_known() { mockEdge(); - assertThat(edgeComputation.getEdges().edges()) + assertThat(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS).edges()) .contains(EdgeWithLiquidityInformation.forUpperBound(EDGE, EDGE.capacity())); } @@ -321,4 +349,8 @@ class EdgeComputationTest { Coins withOnChainReserve = withFeeReserve.subtract(Coins.ofSatoshis(1_000)); return withOnChainReserve.maximum(Coins.NONE); } + + private static Policy policy(int feeRate) { + return new Policy(feeRate, Coins.NONE, true, 40, Coins.ofSatoshis(0)); + } } diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputationTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputationTest.java index da34aca8..25d2fe0a 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputationTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputationTest.java @@ -31,7 +31,6 @@ import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_4; import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -62,23 +61,24 @@ class FlowComputationTest { @Test void solve_no_edge() { - when(edgeComputation.getEdges()).thenReturn(EdgesWithLiquidityInformation.EMPTY); + when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(EdgesWithLiquidityInformation.EMPTY); assertThat(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, Coins.ofSatoshis(1), DEFAULT_PAYMENT_OPTIONS)) .isEqualTo(new Flows()); } @Test void passes_fee_rate_limit_to_get_edges() { - when(edgeComputation.getEdges(anyLong())).thenReturn(EdgesWithLiquidityInformation.EMPTY); - flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, Coins.ofSatoshis(1), PaymentOptions.forFeeRateLimit(123)); - verify(edgeComputation).getEdges(123); + PaymentOptions paymentOptions = PaymentOptions.forFeeRateLimit(123); + when(edgeComputation.getEdges(paymentOptions)).thenReturn(EdgesWithLiquidityInformation.EMPTY); + flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, Coins.ofSatoshis(1), paymentOptions); + verify(edgeComputation).getEdges(paymentOptions); } @Test void solve() { Coins amount = Coins.ofSatoshis(1); EdgeWithLiquidityInformation edge = EdgeWithLiquidityInformation.forUpperBound(EDGE, EDGE.capacity()); - when(edgeComputation.getEdges()).thenReturn(new EdgesWithLiquidityInformation(edge)); + when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(new EdgesWithLiquidityInformation(edge)); Flow expectedFlow = new Flow(EDGE, amount); assertThat(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, amount, DEFAULT_PAYMENT_OPTIONS)) .isEqualTo(new Flows(expectedFlow)); @@ -89,7 +89,7 @@ class FlowComputationTest { when(configurationService.getIntegerValue(QUANTIZATION)).thenReturn(Optional.of(10)); Coins amount = Coins.ofSatoshis(9); EdgeWithLiquidityInformation edge = EdgeWithLiquidityInformation.forUpperBound(EDGE, EDGE.capacity()); - when(edgeComputation.getEdges()).thenReturn(new EdgesWithLiquidityInformation(edge)); + when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(new EdgesWithLiquidityInformation(edge)); assertThat(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, amount, DEFAULT_PAYMENT_OPTIONS)) .isEqualTo(new Flows(new Flow(EDGE, amount))); } @@ -98,7 +98,7 @@ class FlowComputationTest { void solve_avoids_sending_from_depleted_local_channel() { Edge edge1 = new Edge(CHANNEL_ID, PUBKEY, PUBKEY_2, LARGE, POLICY_1); Edge edge2 = new Edge(CHANNEL_ID_2, PUBKEY, PUBKEY_2, SMALL, POLICY_2); - when(edgeComputation.getEdges()).thenReturn(new EdgesWithLiquidityInformation( + when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(new EdgesWithLiquidityInformation( EdgeWithLiquidityInformation.forKnownLiquidity(edge1, Coins.NONE), EdgeWithLiquidityInformation.forUpperBound(edge2, SMALL) )); @@ -114,7 +114,7 @@ class FlowComputationTest { Edge edge1a = new Edge(CHANNEL_ID, PUBKEY_2, PUBKEY_3, LARGE, POLICY_1); Edge edge1b = new Edge(CHANNEL_ID_2, PUBKEY_3, PUBKEY_4, LARGE, POLICY_1); Edge edge2 = new Edge(CHANNEL_ID_3, PUBKEY_2, PUBKEY_4, SMALL, POLICY_2); - when(edgeComputation.getEdges()).thenReturn(new EdgesWithLiquidityInformation( + when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(new EdgesWithLiquidityInformation( EdgeWithLiquidityInformation.forUpperBound(edge1a, amount), EdgeWithLiquidityInformation.forUpperBound(edge1b, LARGE), EdgeWithLiquidityInformation.forUpperBound(edge2, SMALL) @@ -130,7 +130,7 @@ class FlowComputationTest { Edge edge1a = new Edge(CHANNEL_ID, PUBKEY_3, PUBKEY_4, LARGE, POLICY_2); Edge edge1b = new Edge(CHANNEL_ID_2, PUBKEY_4, PUBKEY, LARGE, POLICY_2); Edge edge2 = new Edge(CHANNEL_ID_3, PUBKEY_3, PUBKEY, SMALL, POLICY_1); - when(edgeComputation.getEdges()).thenReturn(new EdgesWithLiquidityInformation( + when(edgeComputation.getEdges(DEFAULT_PAYMENT_OPTIONS)).thenReturn(new EdgesWithLiquidityInformation( EdgeWithLiquidityInformation.forUpperBound(edge1a, Coins.ofSatoshis(5_000_000)), EdgeWithLiquidityInformation.forUpperBound(edge1b, LARGE), EdgeWithLiquidityInformation.forUpperBound(edge2, SMALL) 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 b3b86caa..d38f446e 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 @@ -123,7 +123,7 @@ class MultiPathPaymentSplitterTest { int feeRate = 200; Coins amount = Coins.ofSatoshis(1_000_000); Policy policy = policyFor(feeRate); - PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, PUBKEY_2); + PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, 0, PUBKEY_2); mockFlow(amount, policy, paymentOptions); MultiPathPayment multiPathPayment = @@ -137,7 +137,7 @@ class MultiPathPaymentSplitterTest { int feeRate = 200; Coins amount = Coins.ofSatoshis(2_000_000); Policy policy = policyFor(feeRate); - PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate, PUBKEY_2); + PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate, 0, PUBKEY_2); mockFlow(amount, policy, paymentOptions); MultiPathPayment multiPathPayment = @@ -151,7 +151,7 @@ class MultiPathPaymentSplitterTest { mockExtensionEdge(PUBKEY_3, feeRate); Coins amount = Coins.ofSatoshis(2_000_000); Policy policy = policyFor(0); - PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, PUBKEY_2); + PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, 0, PUBKEY_2); mockFlow(amount, policy, paymentOptions); MultiPathPayment multiPathPayment = @@ -197,11 +197,13 @@ class MultiPathPaymentSplitterTest { @Test void one_flow_has_fee_rate_above_limit_but_average_fee_rate_is_below_limit_including_fees_from_first_hop() { + // The cheaper flows might fail while the more expensive one settles. This is not what we want, so we have + // to disregard all flows. mockExtensionEdge(PUBKEY_4, 0); int feeRate = 200; Coins halfOfAmount = Coins.ofSatoshis(500_000); Coins amount = halfOfAmount.add(halfOfAmount); - PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, PUBKEY_2); + PaymentOptions paymentOptions = PaymentOptions.forTopUp(feeRate - 1, 0, PUBKEY_2); Edge edge1 = new Edge(CHANNEL_ID, PUBKEY, PUBKEY_2, CAPACITY, policyFor(0)); Edge edge2 = new Edge(CHANNEL_ID, PUBKEY, PUBKEY_2, CAPACITY, policyFor(feeRate)); Flow flow1 = new Flow(edge1, halfOfAmount); @@ -348,7 +350,7 @@ class MultiPathPaymentSplitterTest { } private MultiPathPayment attemptTopUpPayment() { - PaymentOptions paymentOptions = PaymentOptions.forTopUp(500, PUBKEY_2); + PaymentOptions paymentOptions = PaymentOptions.forTopUp(500, 123, PUBKEY_2); when(flowComputation.getOptimalFlows(PUBKEY, PUBKEY_2, AMOUNT, paymentOptions)).thenReturn(new Flows(FLOW)); return multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_3, AMOUNT, paymentOptions); } diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/TopUpServiceTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/TopUpServiceTest.java index ed51366b..2609abf6 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/TopUpServiceTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/TopUpServiceTest.java @@ -184,7 +184,7 @@ class TopUpServiceTest { private void assertTopUp(Coins expectedTopUpAmount, Duration expiry) { PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, AMOUNT); verify(grpcInvoices).createPaymentRequest(expectedTopUpAmount, DESCRIPTION, expiry); - PaymentOptions paymentOptions = PaymentOptions.forTopUp(OUR_FEE_RATE, PUBKEY); + PaymentOptions paymentOptions = PaymentOptions.forTopUp(OUR_FEE_RATE, PEER_FEE_RATE, PUBKEY); verify(multiPathPaymentSender).payPaymentRequest(DECODED_PAYMENT_REQUEST, paymentOptions); assertThat(paymentStatus.isPending()).isTrue(); } diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/model/PaymentOptionsTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/model/PaymentOptionsTest.java index d285b128..2dc81038 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/model/PaymentOptionsTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/model/PaymentOptionsTest.java @@ -12,19 +12,25 @@ class PaymentOptionsTest { @Test void forFeeRateWeight() { assertThat(PaymentOptions.forFeeRateWeight(12)) - .isEqualTo(new PaymentOptions(12, Optional.empty(), true, Optional.empty())); + .isEqualTo(new PaymentOptions(12, Optional.empty(), Optional.empty(), true, Optional.empty())); } @Test void forFeeRateLimit() { assertThat(PaymentOptions.forFeeRateLimit(123)) - .isEqualTo(new PaymentOptions(0, Optional.of(123L), true, Optional.empty())); + .isEqualTo(new PaymentOptions(0, Optional.of(123L), Optional.of(123L), true, Optional.empty())); } @Test void forTopUp() { - assertThat(PaymentOptions.forTopUp(123, PUBKEY)) - .isEqualTo(new PaymentOptions(5, Optional.of(123L), false, Optional.of(PUBKEY))); + assertThat(PaymentOptions.forTopUp(123, 100, PUBKEY)) + .isEqualTo(new PaymentOptions(5, Optional.of(123L), Optional.of(23L), false, Optional.of(PUBKEY))); + } + + @Test + void feeRateLimitFirstHops() { + PaymentOptions paymentOptions = PaymentOptions.forTopUp(200, 30, PUBKEY); + assertThat(paymentOptions.feeRateLimitExceptIncomingHops()).contains(170L); } @Test