From d1567f45d88a8cbae3b6f0f3373b2643f0438db5 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Wed, 4 May 2022 20:04:17 +0200 Subject: [PATCH] pay payment requests via pickhardt payments --- .../lndmanagej/grpc/GrpcRouterService.java | 9 ++ .../lndmanagej/grpc/GrpcSendToRoute.java | 91 ++++++++++++++++++ .../cotto/lndmanagej/grpc/NoopObserver.java | 24 +++++ .../lndmanagej/grpc/GrpcSendToRouteTest.java | 92 +++++++++++++++++++ .../lndmanagej/grpc/NoopObserverTest.java | 25 +++++ .../de/cotto/lndmanagej/grpc/StubCreator.java | 13 +++ .../model/DecodedPaymentRequestFixtures.java | 2 +- .../MultiPathPaymentSender.java | 45 +++++++++ .../MultiPathPaymentSenderTest.java | 73 +++++++++++++++ .../PickhardtPaymentsControllerIT.java | 22 +++++ .../PickhardtPaymentsController.java | 21 ++++- .../PickhardtPaymentsControllerTest.java | 21 +++++ 12 files changed, 436 insertions(+), 2 deletions(-) create mode 100644 grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcSendToRoute.java create mode 100644 grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/NoopObserver.java create mode 100644 grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcSendToRouteTest.java create mode 100644 grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/NoopObserverTest.java create mode 100644 pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSender.java create mode 100644 pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSenderTest.java diff --git a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcRouterService.java b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcRouterService.java index 65fb97cd..726ba276 100644 --- a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcRouterService.java +++ b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcRouterService.java @@ -2,6 +2,8 @@ package de.cotto.lndmanagej.grpc; import com.codahale.metrics.annotation.Timed; import de.cotto.lndmanagej.configuration.ConfigurationService; +import io.grpc.stub.StreamObserver; +import lnrpc.HTLCAttempt; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import routerrpc.RouterGrpc; @@ -19,6 +21,7 @@ import java.util.Optional; @Component public class GrpcRouterService extends GrpcBase { private final RouterGrpc.RouterBlockingStub routerStub; + private final RouterGrpc.RouterStub nonBlockingRouterStub; public GrpcRouterService( ConfigurationService configurationService, @@ -26,6 +29,7 @@ public class GrpcRouterService extends GrpcBase { ) throws IOException { super(configurationService, homeDirectory); routerStub = stubCreator.getRouterStub(); + nonBlockingRouterStub = stubCreator.getNonBlockingRouterStub(); } @PreDestroy @@ -44,4 +48,9 @@ public class GrpcRouterService extends GrpcBase { QueryMissionControlRequest request = QueryMissionControlRequest.getDefaultInstance(); return get(() -> routerStub.queryMissionControl(request)); } + + @Timed + public void sendToRoute(RouterOuterClass.SendToRouteRequest request, StreamObserver streamObserver) { + nonBlockingRouterStub.sendToRouteV2(request, streamObserver); + } } diff --git a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcSendToRoute.java b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcSendToRoute.java new file mode 100644 index 00000000..cd306a1c --- /dev/null +++ b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcSendToRoute.java @@ -0,0 +1,91 @@ +package de.cotto.lndmanagej.grpc; + +import com.google.protobuf.ByteString; +import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.DecodedPaymentRequest; +import de.cotto.lndmanagej.model.Edge; +import de.cotto.lndmanagej.model.HexString; +import de.cotto.lndmanagej.model.Route; +import lnrpc.Hop; +import lnrpc.MPPRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import routerrpc.RouterOuterClass.SendToRouteRequest; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class GrpcSendToRoute { + private final GrpcRouterService grpcRouterService; + private final GrpcGetInfo grpcGetInfo; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public GrpcSendToRoute(GrpcRouterService grpcRouterService, GrpcGetInfo grpcGetInfo) { + this.grpcRouterService = grpcRouterService; + this.grpcGetInfo = grpcGetInfo; + } + + public void sendToRoute(Route route, DecodedPaymentRequest decodedPaymentRequest) { + Integer blockHeight = grpcGetInfo.getBlockHeight().orElse(null); + if (blockHeight == null) { + logger.error("Unable to get current block height"); + return; + } + SendToRouteRequest request = buildRequest( + decodedPaymentRequest.paymentHash(), + buildLndRoute(route, blockHeight, decodedPaymentRequest) + ); + grpcRouterService.sendToRoute(request, new NoopObserver<>()); + } + + private lnrpc.Route buildLndRoute(Route route, int blockHeight, DecodedPaymentRequest decodedPaymentRequest) { + int totalTimeLock = route.getTotalTimeLock(blockHeight, decodedPaymentRequest.cltvExpiry()); + Coins totalAmount = route.getAmount().add(route.getFees()); + Coins fees = route.getFees(); + lnrpc.Route.Builder routeBuilder = lnrpc.Route.newBuilder() + .setTotalAmtMsat(totalAmount.milliSatoshis()) + .setTotalFeesMsat(fees.milliSatoshis()) + .setTotalTimeLock(totalTimeLock) + .addAllHops(buildHops(route, blockHeight, decodedPaymentRequest)); + return routeBuilder.build(); + } + + private List buildHops(Route route, int blockHeight, DecodedPaymentRequest decodedPaymentRequest) { + List hops = new ArrayList<>(); + List edges = route.getEdges(); + for (int hopIndex = 0; hopIndex < edges.size(); hopIndex++) { + Edge edge = edges.get(hopIndex); + Hop.Builder hopBuilder = Hop.newBuilder(); + hopBuilder.setChanId(edge.channelId().getShortChannelId()); + hopBuilder.setPubKey(edge.endNode().toString()); + hopBuilder.setAmtToForwardMsat(route.getForwardAmountForHop(hopIndex).milliSatoshis()); + hopBuilder.setExpiry(route.getExpiryForHop(hopIndex, blockHeight, decodedPaymentRequest.cltvExpiry())); + hopBuilder.setFeeMsat(route.getFeeForHop(hopIndex).milliSatoshis()); + hops.add(hopBuilder.build()); + } + addMppRecord(hops, decodedPaymentRequest); + return hops; + } + + private void addMppRecord(List hops, DecodedPaymentRequest decodedPaymentRequest) { + HexString paymentAddress = decodedPaymentRequest.paymentAddress(); + Hop lastHop = hops.remove(hops.size() - 1); + Coins totalAmount = decodedPaymentRequest.amount(); + MPPRecord mppRecord = MPPRecord.newBuilder() + .setTotalAmtMsat(totalAmount.milliSatoshis()) + .setPaymentAddr(ByteString.copyFrom(paymentAddress.getByteArray())) + .build(); + Hop lastHopWithMppRecord = Hop.newBuilder(lastHop).setMppRecord(mppRecord).build(); + hops.add(lastHopWithMppRecord); + } + + private SendToRouteRequest buildRequest(HexString paymentHash, lnrpc.Route lndRoute) { + return SendToRouteRequest.newBuilder() + .setRoute(lndRoute) + .setPaymentHash(ByteString.copyFrom(paymentHash.getByteArray())) + .build(); + } + +} diff --git a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/NoopObserver.java b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/NoopObserver.java new file mode 100644 index 00000000..d44645fc --- /dev/null +++ b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/NoopObserver.java @@ -0,0 +1,24 @@ +package de.cotto.lndmanagej.grpc; + +import io.grpc.stub.StreamObserver; + +class NoopObserver implements StreamObserver { + public NoopObserver() { + // default constructor + } + + @Override + public void onNext(T value) { + // nothing + } + + @Override + public void onError(Throwable throwable) { + // nothing + } + + @Override + public void onCompleted() { + // nothing + } +} diff --git a/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcSendToRouteTest.java b/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcSendToRouteTest.java new file mode 100644 index 00000000..ce4024bb --- /dev/null +++ b/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcSendToRouteTest.java @@ -0,0 +1,92 @@ +package de.cotto.lndmanagej.grpc; + +import com.google.protobuf.ByteString; +import de.cotto.lndmanagej.model.HexString; +import lnrpc.Hop; +import lnrpc.MPPRecord; +import lnrpc.Route; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import routerrpc.RouterOuterClass; + +import java.util.Optional; + +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_5; +import static de.cotto.lndmanagej.model.DecodedPaymentRequestFixtures.DECODED_PAYMENT_REQUEST; +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.model.RouteFixtures.ROUTE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GrpcSendToRouteTest { + private static final int BLOCK_HEIGHT = 800_000; + @InjectMocks + private GrpcSendToRoute grpcSendToRoute; + + @Mock + private GrpcGetInfo grpcGetInfo; + + @Mock + private GrpcRouterService grpcRouterService; + + @Test + void block_height_not_available() { + when(grpcGetInfo.getBlockHeight()).thenReturn(Optional.empty()); + grpcSendToRoute.sendToRoute(ROUTE, DECODED_PAYMENT_REQUEST); + verifyNoInteractions(grpcRouterService); + } + + @Test + void sends_to_converted_route() { + when(grpcGetInfo.getBlockHeight()).thenReturn(Optional.of(BLOCK_HEIGHT)); + grpcSendToRoute.sendToRoute(ROUTE, DECODED_PAYMENT_REQUEST); + RouterOuterClass.SendToRouteRequest expectedRequest = RouterOuterClass.SendToRouteRequest.newBuilder() + .setRoute(Route.newBuilder() + .setTotalTimeLock(ROUTE.getTotalTimeLock(BLOCK_HEIGHT, DECODED_PAYMENT_REQUEST.cltvExpiry())) + .addHops(Hop.newBuilder() + .setChanId(CHANNEL_ID.getShortChannelId()) + .setExpiry(800_184) + .setAmtToForwardMsat(100_020) + .setFeeMsat(20) + .setPubKey(PUBKEY_2.toString()) + .build()) + .addHops(Hop.newBuilder() + .setChanId(CHANNEL_ID_3.getShortChannelId()) + .setExpiry(800_144) + .setAmtToForwardMsat(100_000) + .setFeeMsat(20) + .setPubKey(PUBKEY_3.toString()) + .build()) + .addHops(Hop.newBuilder() + .setChanId(CHANNEL_ID_5.getShortChannelId()) + .setExpiry(800_144) + .setAmtToForwardMsat(100_000) + .setPubKey(PUBKEY_4.toString()) + .setMppRecord(MPPRecord.newBuilder() + .setTotalAmtMsat(123_456) + .setPaymentAddr(toByteString(DECODED_PAYMENT_REQUEST.paymentAddress())) + .build()) + .build()) + .setTotalFeesMsat(40) + .setTotalAmtMsat(100_040) + .build()) + .setPaymentHash(toByteString(DECODED_PAYMENT_REQUEST.paymentHash())) + .build(); + verify(grpcRouterService).sendToRoute(eq(expectedRequest), any()); + } + + private ByteString toByteString(HexString hexString) { + return ByteString.copyFrom(hexString.getByteArray()); + } +} diff --git a/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/NoopObserverTest.java b/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/NoopObserverTest.java new file mode 100644 index 00000000..08ab5b8b --- /dev/null +++ b/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/NoopObserverTest.java @@ -0,0 +1,25 @@ +package de.cotto.lndmanagej.grpc; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; + +class NoopObserverTest { + + private final NoopObserver noopObserver = new NoopObserver<>(); + + @Test + void onNext() { + assertThatCode(() -> noopObserver.onNext("foo")).doesNotThrowAnyException(); + } + + @Test + void onCompleted() { + assertThatCode(noopObserver::onCompleted).doesNotThrowAnyException(); + } + + @Test + void onError() { + assertThatCode(() -> noopObserver.onError(new NullPointerException())).doesNotThrowAnyException(); + } +} diff --git a/grpc-client/src/main/java/de/cotto/lndmanagej/grpc/StubCreator.java b/grpc-client/src/main/java/de/cotto/lndmanagej/grpc/StubCreator.java index c581cee9..666d0b01 100644 --- a/grpc-client/src/main/java/de/cotto/lndmanagej/grpc/StubCreator.java +++ b/grpc-client/src/main/java/de/cotto/lndmanagej/grpc/StubCreator.java @@ -17,6 +17,7 @@ public class StubCreator { private final LightningGrpc.LightningBlockingStub stub; private final LightningGrpc.LightningStub nonBlockingStub; private final RouterGrpc.RouterBlockingStub routerStub; + private final RouterGrpc.RouterStub nonBlockingRouterStub; private final ManagedChannel channel; private final File macaroonFile; private final File certFile; @@ -32,6 +33,7 @@ public class StubCreator { stub = createLightningStub(); nonBlockingStub = createNonBlockingLightningStub(); routerStub = createRouterStub(); + nonBlockingRouterStub = createNonBlockingRouterStub(); } public LightningGrpc.LightningBlockingStub getLightningStub() { @@ -46,6 +48,10 @@ public class StubCreator { return routerStub; } + public RouterGrpc.RouterStub getNonBlockingRouterStub() { + return nonBlockingRouterStub; + } + public void shutdown() { channel.shutdown(); } @@ -75,4 +81,11 @@ public class StubCreator { .withMaxInboundMessageSize(FIFTY_MEGA_BYTE) .withCallCredentials(new MacaroonCallCredential(macaroonFile)); } + + private RouterGrpc.RouterStub createNonBlockingRouterStub() throws IOException { + return RouterGrpc + .newStub(channel) + .withMaxInboundMessageSize(FIFTY_MEGA_BYTE) + .withCallCredentials(new MacaroonCallCredential(macaroonFile)); + } } diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/DecodedPaymentRequestFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/DecodedPaymentRequestFixtures.java index 1e6976e8..409f13bb 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/DecodedPaymentRequestFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/DecodedPaymentRequestFixtures.java @@ -6,7 +6,7 @@ import java.time.ZoneOffset; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; public class DecodedPaymentRequestFixtures { - static final DecodedPaymentRequest DECODED_PAYMENT_REQUEST = new DecodedPaymentRequest( + public static final DecodedPaymentRequest DECODED_PAYMENT_REQUEST = new DecodedPaymentRequest( "some payment request", 144, "description", diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSender.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSender.java new file mode 100644 index 00000000..2ae0d83e --- /dev/null +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSender.java @@ -0,0 +1,45 @@ +package de.cotto.lndmanagej.pickhardtpayments; + +import de.cotto.lndmanagej.grpc.GrpcPayments; +import de.cotto.lndmanagej.grpc.GrpcSendToRoute; +import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.DecodedPaymentRequest; +import de.cotto.lndmanagej.model.Pubkey; +import de.cotto.lndmanagej.model.Route; +import de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPayment; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class MultiPathPaymentSender { + private final GrpcPayments grpcPayments; + private final GrpcSendToRoute grpcSendToRoute; + private final MultiPathPaymentSplitter multiPathPaymentSplitter; + + public MultiPathPaymentSender( + GrpcPayments grpcPayments, + GrpcSendToRoute grpcSendToRoute, + MultiPathPaymentSplitter multiPathPaymentSplitter + ) { + this.grpcPayments = grpcPayments; + this.grpcSendToRoute = grpcSendToRoute; + this.multiPathPaymentSplitter = multiPathPaymentSplitter; + } + + public MultiPathPayment payPaymentRequest(String paymentRequest, int feeRateWeight) { + DecodedPaymentRequest decodedPaymentRequest = grpcPayments.decodePaymentRequest(paymentRequest).orElse(null); + if (decodedPaymentRequest == null) { + return MultiPathPayment.FAILURE; + } + Pubkey destination = decodedPaymentRequest.destination(); + Coins amount = decodedPaymentRequest.amount(); + MultiPathPayment multiPathPayment = + multiPathPaymentSplitter.getMultiPathPaymentTo(destination, amount, feeRateWeight); + List routes = multiPathPayment.routes(); + for (Route route : routes) { + grpcSendToRoute.sendToRoute(route, decodedPaymentRequest); + } + return multiPathPayment; + } +} diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSenderTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSenderTest.java new file mode 100644 index 00000000..a418040b --- /dev/null +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/MultiPathPaymentSenderTest.java @@ -0,0 +1,73 @@ +package de.cotto.lndmanagej.pickhardtpayments; + +import de.cotto.lndmanagej.grpc.GrpcPayments; +import de.cotto.lndmanagej.grpc.GrpcSendToRoute; +import de.cotto.lndmanagej.model.Route; +import de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPayment; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static de.cotto.lndmanagej.model.DecodedPaymentRequestFixtures.DECODED_PAYMENT_REQUEST; +import static de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPaymentFixtures.MULTI_PATH_PAYMENT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MultiPathPaymentSenderTest { + private static final int FEE_RATE_WEIGHT = 213; + private static final String PAYMENT_REQUEST = "abc"; + + @InjectMocks + private MultiPathPaymentSender multiPathPaymentSender; + + @Mock + private GrpcPayments grpcPayments; + + @Mock + private GrpcSendToRoute grpcSendToRoute; + + @Mock + private MultiPathPaymentSplitter multiPathPaymentSplitter; + + @Test + void payment_request_cannot_be_decoded() { + when(grpcPayments.decodePaymentRequest(any())).thenReturn(Optional.empty()); + MultiPathPayment multiPathPayment = multiPathPaymentSender.payPaymentRequest(PAYMENT_REQUEST, FEE_RATE_WEIGHT); + assertThat(multiPathPayment.isFailure()).isTrue(); + verifyNoInteractions(grpcSendToRoute); + } + + @Test + void failure_from_splitter() { + when(grpcPayments.decodePaymentRequest(PAYMENT_REQUEST)).thenReturn(Optional.of(DECODED_PAYMENT_REQUEST)); + when(multiPathPaymentSplitter.getMultiPathPaymentTo(any(), any(), anyInt())) + .thenReturn(MultiPathPayment.FAILURE); + MultiPathPayment multiPathPayment = multiPathPaymentSender.payPaymentRequest(PAYMENT_REQUEST, FEE_RATE_WEIGHT); + assertThat(multiPathPayment.isFailure()).isTrue(); + verifyNoInteractions(grpcSendToRoute); + } + + @Test + void sends_for_each_route() { + when(grpcPayments.decodePaymentRequest(PAYMENT_REQUEST)).thenReturn(Optional.of(DECODED_PAYMENT_REQUEST)); + when(multiPathPaymentSplitter.getMultiPathPaymentTo( + DECODED_PAYMENT_REQUEST.destination(), + DECODED_PAYMENT_REQUEST.amount(), + FEE_RATE_WEIGHT) + ).thenReturn(MULTI_PATH_PAYMENT); + MultiPathPayment multiPathPayment = multiPathPaymentSender.payPaymentRequest(PAYMENT_REQUEST, FEE_RATE_WEIGHT); + assertThat(multiPathPayment.isFailure()).isFalse(); + for (Route route : MULTI_PATH_PAYMENT.routes()) { + verify(grpcSendToRoute).sendToRoute(route, DECODED_PAYMENT_REQUEST); + } + } +} 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 924e766c..df6b2af5 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerIT.java @@ -3,6 +3,7 @@ package de.cotto.lndmanagej.controller; import de.cotto.lndmanagej.controller.dto.ObjectMapperConfiguration; import de.cotto.lndmanagej.model.ChannelIdResolver; import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.pickhardtpayments.MultiPathPaymentSender; import de.cotto.lndmanagej.pickhardtpayments.MultiPathPaymentSplitter; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -32,16 +33,37 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @WebMvcTest(controllers = PickhardtPaymentsController.class) class PickhardtPaymentsControllerIT { private static final String PREFIX = "/beta/pickhardt-payments"; + private static final String PAYMENT_REQUEST = "xxx"; @Autowired private MockMvc mockMvc; @MockBean private MultiPathPaymentSplitter multiPathPaymentSplitter; + @MockBean + private MultiPathPaymentSender multiPathPaymentSender; + @MockBean @SuppressWarnings("unused") private ChannelIdResolver channelIdResolver; + @Test + void payPaymentRequest() throws Exception { + when(multiPathPaymentSender.payPaymentRequest(PAYMENT_REQUEST, DEFAULT_FEE_RATE_WEIGHT)) + .thenReturn(MULTI_PATH_PAYMENT); + String url = "%s/pay-payment-request/%s".formatted(PREFIX, PAYMENT_REQUEST); + mockMvc.perform(get(url)).andExpect(status().isOk()); + } + + @Test + void payPaymentRequest_with_fee_rate_weight() throws Exception { + int feeRateWeight = 987; + when(multiPathPaymentSender.payPaymentRequest(PAYMENT_REQUEST, feeRateWeight)) + .thenReturn(MULTI_PATH_PAYMENT); + String url = "%s/pay-payment-request/%s/fee-rate-weight/%d".formatted(PREFIX, PAYMENT_REQUEST, feeRateWeight); + mockMvc.perform(get(url)).andExpect(status().isOk()); + } + @Test void sendTo() throws Exception { Coins amount = MULTI_PATH_PAYMENT.amount(); 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 5e238eb0..64cd26f6 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/PickhardtPaymentsController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/PickhardtPaymentsController.java @@ -4,6 +4,7 @@ import com.codahale.metrics.annotation.Timed; import de.cotto.lndmanagej.controller.dto.MultiPathPaymentDto; import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.Pubkey; +import de.cotto.lndmanagej.pickhardtpayments.MultiPathPaymentSender; import de.cotto.lndmanagej.pickhardtpayments.MultiPathPaymentSplitter; import de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPayment; import org.springframework.web.bind.annotation.GetMapping; @@ -17,9 +18,27 @@ import static de.cotto.lndmanagej.pickhardtpayments.PickhardtPaymentsConfigurati @RequestMapping("/beta/pickhardt-payments/") public class PickhardtPaymentsController { private final MultiPathPaymentSplitter multiPathPaymentSplitter; + private final MultiPathPaymentSender multiPathPaymentSender; - public PickhardtPaymentsController(MultiPathPaymentSplitter multiPathPaymentSplitter) { + public PickhardtPaymentsController( + MultiPathPaymentSplitter multiPathPaymentSplitter, + MultiPathPaymentSender multiPathPaymentSender + ) { this.multiPathPaymentSplitter = multiPathPaymentSplitter; + this.multiPathPaymentSender = multiPathPaymentSender; + } + + @Timed + @GetMapping("/pay-payment-request/{paymentRequest}") + public MultiPathPaymentDto payPaymentRequest(@PathVariable String paymentRequest) { + return payPaymentRequest(paymentRequest, DEFAULT_FEE_RATE_WEIGHT); + } + + @Timed + @GetMapping("/pay-payment-request/{paymentRequest}/fee-rate-weight/{feeRateWeight}") + public MultiPathPaymentDto payPaymentRequest(@PathVariable String paymentRequest, @PathVariable int feeRateWeight) { + MultiPathPayment multiPathPayment = multiPathPaymentSender.payPaymentRequest(paymentRequest, feeRateWeight); + return MultiPathPaymentDto.fromModel(multiPathPayment); } @Timed 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 562ca120..bfac702c 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/PickhardtPaymentsControllerTest.java @@ -2,6 +2,7 @@ package de.cotto.lndmanagej.controller; import de.cotto.lndmanagej.controller.dto.MultiPathPaymentDto; import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.pickhardtpayments.MultiPathPaymentSender; import de.cotto.lndmanagej.pickhardtpayments.MultiPathPaymentSplitter; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,12 +20,32 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class PickhardtPaymentsControllerTest { + private static final String PAYMENT_REQUEST = "xxx"; @InjectMocks private PickhardtPaymentsController controller; @Mock private MultiPathPaymentSplitter multiPathPaymentSplitter; + @Mock + private MultiPathPaymentSender multiPathPaymentSender; + + @Test + void payPaymentRequest() { + when(multiPathPaymentSender.payPaymentRequest(PAYMENT_REQUEST, DEFAULT_FEE_RATE_WEIGHT)) + .thenReturn(MULTI_PATH_PAYMENT); + assertThat(controller.payPaymentRequest(PAYMENT_REQUEST)) + .isEqualTo(MultiPathPaymentDto.fromModel(MULTI_PATH_PAYMENT)); + } + + @Test + void payPaymentRequest_with_fee_rate_weight() { + when(multiPathPaymentSender.payPaymentRequest(PAYMENT_REQUEST, 456)) + .thenReturn(MULTI_PATH_PAYMENT); + assertThat(controller.payPaymentRequest(PAYMENT_REQUEST, 456)) + .isEqualTo(MultiPathPaymentDto.fromModel(MULTI_PATH_PAYMENT)); + } + @Test void sendTo() { when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, Coins.ofSatoshis(456), DEFAULT_FEE_RATE_WEIGHT))