From e09fe3f1db2a72df516dd1daf13b5b8a6ddf0f5a Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Mon, 14 Aug 2023 18:21:45 +0200 Subject: [PATCH] add first hop filter to top-up fixes #86 --- PickhardtPayments.md | 5 +++ .../pickhardtpayments/EdgeComputation.java | 19 +++++++++ .../pickhardtpayments/TopUpService.java | 41 ++++++++++++++----- .../model/PaymentOptions.java | 25 ++++++++++- .../EdgeComputationTest.java | 18 ++++++++ .../pickhardtpayments/TopUpServiceTest.java | 29 +++++++++---- .../model/PaymentOptionsTest.java | 18 +++++++- .../controller/PaymentsControllerIT.java | 39 +++++++++++++++--- .../controller/PaymentsController.java | 32 ++++++++++++++- .../controller/dto/PaymentOptionsDto.java | 1 + .../controller/PaymentsControllerTest.java | 26 ++++++++++-- .../controller/dto/PaymentOptionsDtoTest.java | 2 + 12 files changed, 223 insertions(+), 32 deletions(-) diff --git a/PickhardtPayments.md b/PickhardtPayments.md index 36c0bfac..5880fe08 100644 --- a/PickhardtPayments.md +++ b/PickhardtPayments.md @@ -122,10 +122,15 @@ The response shows a somewhat readable representation of the payment progress, i node Z and node A, the whole payment fails (it is not attempted). * Invoices (payment requests) created for top-up payments expiry after 30 minutes. This value can be configured as `expiry_seconds=`. +* HTTP `GET`: `/api/payments/top-up/{pubkey}/amount/{amount}/via/{pubkey}` + * As before, but the pubkey specified after "via" is used for the first hop. As such, this can be used to reduce + outbound liquidity to the "via" peer. * HTTP `POST`: `/api/payments/top-up/{pubkey}/amount/{amount}` * allows you to lower the fee rate limit (values higher than the computed fee rate limit are ignored) * allows you to specify a different fee rate weight * The value provided as `ignoreFeesForOwnChannels` is ignored, for top-up such fees are never ignored +* HTTP `POST`: `/api/payments/top-up/{pubkey}/amount/{amount}/via/{pubkey}` + * As above, see corresponding GET endpoint The threshold, i.e. the minimum difference between the current local balance and the requested amount, defaults to 10,000sat. You can configure this value by setting `threshold_sat=` in the configuration file. 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 a6f0bacf..c7c8458c 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 @@ -127,6 +127,9 @@ public class EdgeComputation { if (feeRate >= feeRateLimit) { return true; } + if (isEdgeToUnwantedFirstHop(channelEdge, paymentOptions, pubkey)) { + return true; + } if (isIncomingEdge(channelEdge, pubkey)) { return false; } @@ -137,6 +140,22 @@ public class EdgeComputation { return feeRate >= feeRateLimitFirstHops; } + private boolean isEdgeToUnwantedFirstHop( + DirectedChannelEdge channelEdge, + PaymentOptions paymentOptions, + Pubkey pubkey + ) { + boolean isOutgoingEdge = pubkey.equals(channelEdge.source()); + if (!isOutgoingEdge) { + return false; + } + Pubkey peerForFirstHop = paymentOptions.peerForFirstHop().orElse(null); + if (peerForFirstHop == null) { + return false; + } + return !peerForFirstHop.equals(channelEdge.target()); + } + private boolean isIncomingEdge(DirectedChannelEdge channelEdge, Pubkey ownPubkey) { return ownPubkey.equals(channelEdge.target()); } 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 6c99632c..39a27a65 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 @@ -51,7 +51,12 @@ public class TopUpService { this.policyService = policyService; } - public PaymentStatus topUp(Pubkey pubkey, Coins amount, PaymentOptions paymentOptionsFromRequest) { + public PaymentStatus topUp( + Pubkey pubkey, + Optional peerForFirstHop, + Coins amount, + PaymentOptions paymentOptionsFromRequest + ) { if (noChannelWith(pubkey)) { String alias = nodeService.getAlias(pubkey); return PaymentStatus.createFailure("No channel with %s (%s)".formatted(pubkey, alias)); @@ -71,10 +76,15 @@ public class TopUpService { return PaymentStatus.createFailure(reason); } - return sendPayment(pubkey, topUpAmount, paymentOptionsFromRequest); + return sendPayment(pubkey, peerForFirstHop, topUpAmount, paymentOptionsFromRequest); } - private PaymentStatus sendPayment(Pubkey pubkey, Coins topUpAmount, PaymentOptions paymentOptionsFromRequest) { + private PaymentStatus sendPayment( + Pubkey pubkey, + Optional peerForFirstHop, + Coins topUpAmount, + PaymentOptions paymentOptions + ) { long ourFeeRate = policyService.getMinimumFeeRateTo(pubkey).orElse(0L); long peerFeeRate = policyService.getMinimumFeeRateFrom(pubkey).orElse(0L); if (peerFeeRate >= ourFeeRate) { @@ -88,8 +98,10 @@ public class TopUpService { String alias = nodeService.getAlias(pubkey); return PaymentStatus.createFailure("Unable to create payment request (%s, %s)".formatted(pubkey, alias)); } - PaymentOptions paymentOptions = getPaymentOptions(pubkey, ourFeeRate, peerFeeRate, paymentOptionsFromRequest); - return multiPathPaymentSender.payPaymentRequest(paymentRequest, paymentOptions); + return multiPathPaymentSender.payPaymentRequest( + paymentRequest, + getPaymentOptions(pubkey, peerForFirstHop, ourFeeRate, peerFeeRate, paymentOptions) + ); } private Coins getThreshold() { @@ -117,16 +129,25 @@ public class TopUpService { private PaymentOptions getPaymentOptions( Pubkey pubkey, + Optional peerForFirstHop, long ourFeeRate, long peerFeeRate, - PaymentOptions paymentOptionsFromRequest + PaymentOptions paymentOptions ) { - long feeRateLimit = Math.min(ourFeeRate, paymentOptionsFromRequest.feeRateLimit().orElse(Long.MAX_VALUE)); - return PaymentOptions.forTopUp( - paymentOptionsFromRequest.feeRateWeight().orElse(5), + int feeRateWeight = paymentOptions.feeRateWeight().orElse(5); + long feeRateLimit = Math.min(ourFeeRate, paymentOptions.feeRateLimit().orElse(Long.MAX_VALUE)); + + return peerForFirstHop.map(value -> PaymentOptions.forTopUp( + feeRateWeight, + feeRateLimit, + peerFeeRate, + pubkey, + value + )).orElseGet(() -> PaymentOptions.forTopUp( + feeRateWeight, feeRateLimit, peerFeeRate, pubkey - ); + )); } } 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 f8c698ab..acc6fe28 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 @@ -9,7 +9,8 @@ public record PaymentOptions( Optional feeRateLimit, Optional feeRateLimitExceptIncomingHops, boolean ignoreFeesForOwnChannels, - Optional peer + Optional peer, + Optional peerForFirstHop ) { public static final PaymentOptions DEFAULT_PAYMENT_OPTIONS = forFeeRateWeight(0); @@ -19,6 +20,7 @@ public record PaymentOptions( Optional.empty(), Optional.empty(), true, + Optional.empty(), Optional.empty() ); } @@ -29,6 +31,7 @@ public record PaymentOptions( Optional.of(feeRateLimit), Optional.empty(), true, + Optional.empty(), Optional.empty() ); } @@ -39,7 +42,25 @@ public record PaymentOptions( Optional.of(feeRateLimit), Optional.of(Math.max(0, feeRateLimit - peerFeeRate)), false, - Optional.of(peer) + Optional.of(peer), + Optional.empty() + ); + } + + public static PaymentOptions forTopUp( + int feeRateWeight, + long feeRateLimit, + long peerFeeRate, + Pubkey peer, + Pubkey peerForFirstHop + ) { + return new PaymentOptions( + Optional.of(feeRateWeight), + Optional.of(feeRateLimit), + Optional.of(Math.max(0, feeRateLimit - peerFeeRate)), + false, + Optional.of(peer), + Optional.of(peerForFirstHop) ); } } 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 948ef912..144793c7 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 @@ -13,6 +13,7 @@ import de.cotto.lndmanagej.model.LocalOpenChannel; import de.cotto.lndmanagej.model.LocalOpenChannelFixtures; 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; @@ -44,6 +45,7 @@ import static de.cotto.lndmanagej.model.PolicyFixtures.POLICY_DISABLED; 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_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; @@ -312,6 +314,22 @@ class EdgeComputationTest { .contains(EdgeWithLiquidityInformation.forUpperBound(EDGE, EDGE.capacity())); } + @Test + void ignores_other_peers_if_peer_for_first_hop_is_set() { + Pubkey ownNode = PUBKEY_4; + Pubkey peerForFirstHop = PUBKEY_3; + + PaymentOptions paymentOptions = + PaymentOptions.forTopUp(FEE_RATE_WEIGHT, 999, 0, PUBKEY_2, peerForFirstHop); + DirectedChannelEdge edge1 = new DirectedChannelEdge(CHANNEL_ID, CAPACITY, ownNode, peerForFirstHop, POLICY_1); + DirectedChannelEdge edge2 = new DirectedChannelEdge(CHANNEL_ID, CAPACITY, ownNode, PUBKEY, POLICY_1); + when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(edge1, edge2))); + + EdgesWithLiquidityInformation edges = edgeComputation.getEdges(paymentOptions, MAX_TIME_LOCK_DELTA); + + assertThat(edges.edges().stream().map(EdgeWithLiquidityInformation::endNode)).containsExactly(peerForFirstHop); + } + @Test void getEdgeWithLiquidityInformation_default() { when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4); 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 60896839..9c2be225 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 @@ -178,6 +178,7 @@ class TopUpServiceTest { Optional.of(feeRateLimit), Optional.empty(), true, + Optional.empty(), Optional.empty() ); PaymentOptions expected = new PaymentOptions( @@ -185,7 +186,8 @@ class TopUpServiceTest { Optional.of(feeRateLimit), Optional.of(feeRateLimit - peerFeeRate), false, - Optional.of(PUBKEY) + Optional.of(PUBKEY), + Optional.empty() ); when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE); assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected); @@ -203,6 +205,7 @@ class TopUpServiceTest { Optional.of(feeRateLimit), Optional.empty(), true, + Optional.empty(), Optional.empty() ); PaymentOptions expected = new PaymentOptions( @@ -210,7 +213,8 @@ class TopUpServiceTest { Optional.of(ourFeeRate), Optional.of(ourFeeRate - peerFeeRate), false, - Optional.of(PUBKEY) + Optional.of(PUBKEY), + Optional.empty() ); when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE); assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected); @@ -228,6 +232,7 @@ class TopUpServiceTest { Optional.of(feeRateLimit), Optional.empty(), true, + Optional.empty(), Optional.empty() ); PaymentOptions expected = new PaymentOptions( @@ -235,7 +240,8 @@ class TopUpServiceTest { Optional.of(feeRateLimit), Optional.of(feeRateLimit - peerFeeRate), false, - Optional.of(PUBKEY) + Optional.of(PUBKEY), + Optional.empty() ); when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE); assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected); @@ -253,6 +259,7 @@ class TopUpServiceTest { Optional.of(feeRateLimit), Optional.empty(), true, + Optional.empty(), Optional.empty() ); PaymentOptions expected = new PaymentOptions( @@ -260,7 +267,8 @@ class TopUpServiceTest { Optional.of(ourFeeRate), Optional.of(ourFeeRate - peerFeeRate), false, - Optional.of(PUBKEY) + Optional.of(PUBKEY), + Optional.empty() ); when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE); assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected); @@ -278,6 +286,7 @@ class TopUpServiceTest { Optional.of(feeRateLimit), Optional.empty(), true, + Optional.empty(), Optional.empty() ); PaymentOptions expected = new PaymentOptions( @@ -285,7 +294,8 @@ class TopUpServiceTest { Optional.of(feeRateLimit), Optional.of(0L), false, - Optional.of(PUBKEY) + Optional.of(PUBKEY), + Optional.empty() ); when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE); assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected); @@ -299,6 +309,7 @@ class TopUpServiceTest { Optional.empty(), Optional.empty(), true, + Optional.empty(), Optional.empty() ); PaymentOptions expected = new PaymentOptions( @@ -306,7 +317,8 @@ class TopUpServiceTest { Optional.of(OUR_FEE_RATE), Optional.of(OUR_FEE_RATE - PEER_FEE_RATE), false, - Optional.of(PUBKEY) + Optional.of(PUBKEY), + Optional.empty() ); when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE); assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected); @@ -339,6 +351,7 @@ class TopUpServiceTest { Optional.empty(), Optional.empty(), true, + Optional.empty(), Optional.empty() ); assertTopUp(expectedTopUpAmount, expiry, emptyPaymentOptions, paymentOptions); @@ -350,14 +363,14 @@ class TopUpServiceTest { PaymentOptions givenPaymentOptions, PaymentOptions expectedPaymentOptions ) { - PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, AMOUNT, givenPaymentOptions); + PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, Optional.empty(), AMOUNT, givenPaymentOptions); verify(grpcInvoices).createPaymentRequest(expectedTopUpAmount, DESCRIPTION, expiry); verify(multiPathPaymentSender).payPaymentRequest(DECODED_PAYMENT_REQUEST, expectedPaymentOptions); assertThat(paymentStatus.isPending()).isTrue(); } private void assertFailure(String reason) { - PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, AMOUNT, DEFAULT_PAYMENT_OPTIONS); + PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, Optional.empty(), AMOUNT, DEFAULT_PAYMENT_OPTIONS); assertThat(paymentStatus.isFailure()).isTrue(); assertThat(readAll(paymentStatus)).map(InstantWithString::string).containsExactly(reason); verifyNoInteractions(multiPathPaymentSender); 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 3e7a9e75..bc850087 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 @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import java.util.Optional; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS; import static org.assertj.core.api.Assertions.assertThat; @@ -19,6 +20,7 @@ class PaymentOptionsTest { Optional.empty(), Optional.empty(), true, + Optional.empty(), Optional.empty() )); } @@ -30,6 +32,7 @@ class PaymentOptionsTest { Optional.of(123L), Optional.empty(), true, + Optional.empty(), Optional.empty() )); } @@ -41,7 +44,20 @@ class PaymentOptionsTest { Optional.of(123L), Optional.of(23L), false, - Optional.of(PUBKEY) + Optional.of(PUBKEY), + Optional.empty() + )); + } + + @Test + void forTopUp_with_peer_for_first_hop() { + assertThat(PaymentOptions.forTopUp(FEE_RATE_WEIGHT, 123, 100, PUBKEY, PUBKEY_2)).isEqualTo(new PaymentOptions( + Optional.of(FEE_RATE_WEIGHT), + Optional.of(123L), + Optional.of(23L), + false, + Optional.of(PUBKEY), + Optional.of(PUBKEY_2) )); } diff --git a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/PaymentsControllerIT.java b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/PaymentsControllerIT.java index 429908e0..3bee21f3 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/PaymentsControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/PaymentsControllerIT.java @@ -48,6 +48,7 @@ class PaymentsControllerIT { Optional.of(999L), Optional.empty(), false, + Optional.empty(), Optional.empty() ); private static final int FINAL_CLTV_EXPIRY = 0; @@ -181,29 +182,55 @@ class PaymentsControllerIT { @Test void topUp() { - when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus); + when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus); PaymentOptions emptyPaymentOptions = new PaymentOptions( Optional.empty(), Optional.empty(), Optional.empty(), true, + Optional.empty(), Optional.empty() ); webTestClient.get().uri(url).exchange().expectStatus().isOk(); - verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), emptyPaymentOptions); + verify(topUpService).topUp(PUBKEY, Optional.empty(), Coins.ofSatoshis(123), emptyPaymentOptions); + } + + @Test + void topUp_with_peer_for_first_hop() { + when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus); + PaymentOptions emptyPaymentOptions = new PaymentOptions( + Optional.empty(), + Optional.empty(), + Optional.empty(), + true, + Optional.empty(), + Optional.empty() + ); + String uri = "%s/top-up/%s/amount/%s/via/%s".formatted(PREFIX, PUBKEY, "123", PUBKEY_2); + webTestClient.get().uri(uri).exchange().expectStatus().isOk(); + verify(topUpService).topUp(PUBKEY, Optional.of(PUBKEY_2), Coins.ofSatoshis(123), emptyPaymentOptions); } @Test void with_payment_options() { - when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus); + when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus); webTestClient.post().uri(url).contentType(APPLICATION_JSON).bodyValue(DTO_AS_STRING).exchange() .expectStatus().isOk(); - verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), PAYMENT_OPTIONS); + verify(topUpService).topUp(PUBKEY, Optional.empty(), Coins.ofSatoshis(123), PAYMENT_OPTIONS); + } + + @Test + void with_peer_for_first_hop_and_payment_options() { + when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus); + String uri = "%s/top-up/%s/amount/%s/via/%s".formatted(PREFIX, PUBKEY, "123", PUBKEY_2); + webTestClient.post().uri(uri).contentType(APPLICATION_JSON).bodyValue(DTO_AS_STRING).exchange() + .expectStatus().isOk(); + verify(topUpService).topUp(PUBKEY, Optional.of(PUBKEY_2), Coins.ofSatoshis(123), PAYMENT_OPTIONS); } @Test void no_linebreaks_within_line() { - when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus); + when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus); webTestClient.get().uri(url).exchange().expectBody(String.class).value(string -> assertThat(string.substring(0, string.length() - 1)).doesNotContain("\n") ); @@ -211,7 +238,7 @@ class PaymentsControllerIT { @Test void linebreak_at_end_of_line() { - when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus); + when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus); webTestClient.get().uri(url).exchange().expectBody(String.class).value(string -> assertThat(string).endsWith("\n") ); diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/PaymentsController.java b/web/src/main/java/de/cotto/lndmanagej/controller/PaymentsController.java index 2eb36caa..b5d028b0 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/PaymentsController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/PaymentsController.java @@ -22,6 +22,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; +import java.util.Optional; + import static org.springframework.http.MediaType.APPLICATION_NDJSON; @RestController @@ -119,6 +121,16 @@ public class PaymentsController { return topUp(pubkey, amount, new PaymentOptionsDto()); } + @Timed + @GetMapping("/top-up/{pubkey}/amount/{amount}/via/{peerForFirstHop}") + public ResponseEntity> topUp( + @PathVariable Pubkey pubkey, + @PathVariable long amount, + @PathVariable Pubkey peerForFirstHop + ) { + return topUp(pubkey, amount, peerForFirstHop, new PaymentOptionsDto()); + } + @Timed @PostMapping("/top-up/{pubkey}/amount/{amount}") public ResponseEntity> topUp( @@ -126,7 +138,25 @@ public class PaymentsController { @PathVariable long amount, @RequestBody PaymentOptionsDto paymentOptionsDto ) { - PaymentStatus paymentStatus = topUpService.topUp(pubkey, Coins.ofSatoshis(amount), paymentOptionsDto.toModel()); + PaymentStatus paymentStatus = + topUpService.topUp(pubkey, Optional.empty(), Coins.ofSatoshis(amount), paymentOptionsDto.toModel()); + return toStream(paymentStatus); + } + + @Timed + @PostMapping("/top-up/{pubkey}/amount/{amount}/via/{peerForFirstHop}") + public ResponseEntity> topUp( + @PathVariable Pubkey pubkey, + @PathVariable long amount, + @PathVariable Pubkey peerForFirstHop, + @RequestBody PaymentOptionsDto paymentOptionsDto + ) { + PaymentStatus paymentStatus = topUpService.topUp( + pubkey, + Optional.of(peerForFirstHop), + Coins.ofSatoshis(amount), + paymentOptionsDto.toModel() + ); return toStream(paymentStatus); } diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/dto/PaymentOptionsDto.java b/web/src/main/java/de/cotto/lndmanagej/controller/dto/PaymentOptionsDto.java index 0faef225..d6e356a2 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/dto/PaymentOptionsDto.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/dto/PaymentOptionsDto.java @@ -31,6 +31,7 @@ public class PaymentOptionsDto { Optional.ofNullable(feeRateLimit), Optional.empty(), ignoreFeesForOwnChannels, + Optional.empty(), Optional.empty() ); } diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/PaymentsControllerTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/PaymentsControllerTest.java index f6a4aee9..55a34500 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/PaymentsControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/PaymentsControllerTest.java @@ -25,6 +25,7 @@ import static de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPaymentFixtur 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.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -44,6 +45,7 @@ class PaymentsControllerTest { Optional.of(999L), Optional.empty(), false, + Optional.empty(), Optional.empty() ); PAYMENT_OPTIONS_DTO = new PaymentOptionsDto(); @@ -145,23 +147,39 @@ class PaymentsControllerTest { @Test void topUp() { - when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus); + when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus); PaymentOptions emptyPaymentOptions = new PaymentOptions( Optional.empty(), Optional.empty(), Optional.empty(), true, + Optional.empty(), Optional.empty() ); assertThat(controller.topUp(PUBKEY, 123).getStatusCode()).isEqualTo(HttpStatus.OK); - verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), emptyPaymentOptions); + verify(topUpService).topUp(PUBKEY, Optional.empty(), Coins.ofSatoshis(123), emptyPaymentOptions); + } + + @Test + void topUp_with_peer_for_first_hop() { + when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus); + assertThat(controller.topUp(PUBKEY, 123, PUBKEY_2).getStatusCode()).isEqualTo(HttpStatus.OK); + verify(topUpService).topUp(any(), eq(Optional.of(PUBKEY_2)), any(), any()); } @Test void topUp_with_payment_options() { - when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus); + when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus); assertThat(controller.topUp(PUBKEY, 123, PAYMENT_OPTIONS_DTO).getStatusCode()).isEqualTo(HttpStatus.OK); - verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), PAYMENT_OPTIONS); + verify(topUpService).topUp(PUBKEY, Optional.empty(), Coins.ofSatoshis(123), PAYMENT_OPTIONS); + } + + @Test + void topUp_with_peer_for_first_hop_and_payment_options() { + when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus); + assertThat(controller.topUp(PUBKEY, 123, PUBKEY_2, PAYMENT_OPTIONS_DTO).getStatusCode()) + .isEqualTo(HttpStatus.OK); + verify(topUpService).topUp(PUBKEY, Optional.of(PUBKEY_2), Coins.ofSatoshis(123), PAYMENT_OPTIONS); } @Test diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/dto/PaymentOptionsDtoTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/dto/PaymentOptionsDtoTest.java index beece5b7..8bcff870 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/dto/PaymentOptionsDtoTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/dto/PaymentOptionsDtoTest.java @@ -31,6 +31,7 @@ class PaymentOptionsDtoTest { Optional.of(feeRateLimit), Optional.empty(), DEFAULT_PAYMENT_OPTIONS.ignoreFeesForOwnChannels(), + Optional.empty(), Optional.empty() ); assertThat(dto.toModel()).isEqualTo(expected); @@ -45,6 +46,7 @@ class PaymentOptionsDtoTest { Optional.empty(), Optional.empty(), false, + Optional.empty(), Optional.empty() ); assertThat(dto.toModel()).isEqualTo(expected);