From 2f8765ba89965196b555beb3dee9b95cc3064d76 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Fri, 5 Nov 2021 16:18:13 +0100 Subject: [PATCH] log settled and failed HTLCs --- application/build.gradle | 10 +- .../java/de/cotto/lndmanagej/HtlcLogger.java | 29 + .../java/de/cotto/lndmanagej/InfoLogger.java | 2 +- .../src/main/resources/application.properties | 1 + .../de/cotto/lndmanagej/HtlcLoggerTest.java | 44 + .../de/cotto/lndmanagej/InfoLoggerTest.java | 2 +- .../graph/model/ChannelFixtures.java | 16 - .../graph/model/ChannelIdFixtures.java | 5 - grpc-adapter/build.gradle | 2 + .../cotto/lndmanagej/grpc/GrpcHtlcEvents.java | 109 +++ .../de/cotto/lndmanagej/grpc/GrpcService.java | 23 +- .../lndmanagej/grpc/GrpcHtlcEventsTest.java | 207 +++++ .../de/cotto/lndmanagej/grpc/StubCreator.java | 14 + grpc-client/src/main/proto/router.proto | 800 ++++++++++++++++++ {graph => model}/build.gradle | 0 .../de/cotto/lndmanagej}/model/Channel.java | 2 +- .../de/cotto/lndmanagej}/model/ChannelId.java | 7 +- .../de/cotto/lndmanagej}/model/Coins.java | 2 +- .../lndmanagej/model/ForwardAttempt.java | 112 +++ .../lndmanagej/model/ForwardFailure.java | 38 + .../cotto/lndmanagej/model/HtlcDetails.java | 123 +++ .../java/de/cotto/lndmanagej}/model/Node.java | 2 +- .../lndmanagej/model/SettledForward.java | 38 + .../lndmanagej}/model/ChannelIdTest.java | 8 +- .../cotto/lndmanagej}/model/ChannelTest.java | 23 +- .../de/cotto/lndmanagej}/model/CoinsTest.java | 2 +- .../lndmanagej/model/ForwardAttemptTest.java | 48 ++ .../lndmanagej/model/ForwardFailureTest.java | 31 + .../lndmanagej/model/HtlcDetailsTest.java | 61 ++ .../de/cotto/lndmanagej}/model/NodeTest.java | 6 +- .../lndmanagej/model/SettledForwardTest.java | 26 + .../lndmanagej/model/ChannelFixtures.java | 15 + .../lndmanagej/model/ChannelIdFixtures.java | 7 + .../model/ForwardAttemptFixtures.java | 13 + .../model/ForwardFailureFixtures.java | 8 + .../lndmanagej/model/HtlcDetailsFixtures.java | 14 + .../cotto/lndmanagej}/model/NodeFixtures.java | 2 +- .../model/SettledForwardFixtures.java | 8 + settings.gradle | 2 +- 39 files changed, 1808 insertions(+), 54 deletions(-) create mode 100644 application/src/main/java/de/cotto/lndmanagej/HtlcLogger.java create mode 100644 application/src/test/java/de/cotto/lndmanagej/HtlcLoggerTest.java delete mode 100644 graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/ChannelFixtures.java delete mode 100644 graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/ChannelIdFixtures.java create mode 100644 grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcHtlcEvents.java create mode 100644 grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcHtlcEventsTest.java create mode 100644 grpc-client/src/main/proto/router.proto rename {graph => model}/build.gradle (100%) rename {graph/src/main/java/de/cotto/lndmanagej/graph => model/src/main/java/de/cotto/lndmanagej}/model/Channel.java (98%) rename {graph/src/main/java/de/cotto/lndmanagej/graph => model/src/main/java/de/cotto/lndmanagej}/model/ChannelId.java (92%) rename {graph/src/main/java/de/cotto/lndmanagej/graph => model/src/main/java/de/cotto/lndmanagej}/model/Coins.java (98%) create mode 100644 model/src/main/java/de/cotto/lndmanagej/model/ForwardAttempt.java create mode 100644 model/src/main/java/de/cotto/lndmanagej/model/ForwardFailure.java create mode 100644 model/src/main/java/de/cotto/lndmanagej/model/HtlcDetails.java rename {graph/src/main/java/de/cotto/lndmanagej/graph => model/src/main/java/de/cotto/lndmanagej}/model/Node.java (98%) create mode 100644 model/src/main/java/de/cotto/lndmanagej/model/SettledForward.java rename {graph/src/test/java/de/cotto/lndmanagej/graph => model/src/test/java/de/cotto/lndmanagej}/model/ChannelIdTest.java (93%) rename {graph/src/test/java/de/cotto/lndmanagej/graph => model/src/test/java/de/cotto/lndmanagej}/model/ChannelTest.java (78%) rename {graph/src/test/java/de/cotto/lndmanagej/graph => model/src/test/java/de/cotto/lndmanagej}/model/CoinsTest.java (99%) create mode 100644 model/src/test/java/de/cotto/lndmanagej/model/ForwardAttemptTest.java create mode 100644 model/src/test/java/de/cotto/lndmanagej/model/ForwardFailureTest.java create mode 100644 model/src/test/java/de/cotto/lndmanagej/model/HtlcDetailsTest.java rename {graph/src/test/java/de/cotto/lndmanagej/graph => model/src/test/java/de/cotto/lndmanagej}/model/NodeTest.java (95%) create mode 100644 model/src/test/java/de/cotto/lndmanagej/model/SettledForwardTest.java create mode 100644 model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelFixtures.java create mode 100644 model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelIdFixtures.java create mode 100644 model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardAttemptFixtures.java create mode 100644 model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardFailureFixtures.java create mode 100644 model/src/testFixtures/java/de/cotto/lndmanagej/model/HtlcDetailsFixtures.java rename {graph/src/testFixtures/java/de/cotto/lndmanagej/graph => model/src/testFixtures/java/de/cotto/lndmanagej}/model/NodeFixtures.java (96%) create mode 100644 model/src/testFixtures/java/de/cotto/lndmanagej/model/SettledForwardFixtures.java diff --git a/application/build.gradle b/application/build.gradle index 347e8393..f1cec486 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -4,9 +4,9 @@ plugins { dependencies { implementation project(':grpc-adapter') - implementation project(':graph') + implementation project(':model') runtimeOnly 'org.postgresql:postgresql' - testImplementation testFixtures(project(':graph')) + testImplementation testFixtures(project(':model')) } bootJar { @@ -18,13 +18,13 @@ jacocoTestCoverageVerification { rules.forEach {rule -> rule.limits.forEach {limit -> if (limit.counter == 'CLASS') { - limit.minimum = 0.5 + limit.minimum = 0.6 } if (limit.counter == 'INSTRUCTION') { - limit.minimum = 0.81 + limit.minimum = 0.94 } if (limit.counter == 'METHOD') { - limit.minimum = 0.5 + limit.minimum = 0.7 } } } diff --git a/application/src/main/java/de/cotto/lndmanagej/HtlcLogger.java b/application/src/main/java/de/cotto/lndmanagej/HtlcLogger.java new file mode 100644 index 00000000..e6a78c5e --- /dev/null +++ b/application/src/main/java/de/cotto/lndmanagej/HtlcLogger.java @@ -0,0 +1,29 @@ +package de.cotto.lndmanagej; + +import de.cotto.lndmanagej.grpc.GrpcHtlcEvents; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class HtlcLogger { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final GrpcHtlcEvents grpcHtlcEvents; + + public HtlcLogger(GrpcHtlcEvents grpcHtlcEvents) { + this.grpcHtlcEvents = grpcHtlcEvents; + } + + @Scheduled(fixedDelay = 1_000) + public void logForwardFailures() { + grpcHtlcEvents.getForwardFailures() + .forEach(forwardFailure -> logger.info("Forward Failure: {}", forwardFailure)); + } + + @Scheduled(fixedDelay = 1_000) + public void logSettledForwards() { + grpcHtlcEvents.getSettledForwards() + .forEach(settledForward -> logger.info("Settled Forward: {}", settledForward)); + } +} diff --git a/application/src/main/java/de/cotto/lndmanagej/InfoLogger.java b/application/src/main/java/de/cotto/lndmanagej/InfoLogger.java index ae9be2d0..56773fb5 100644 --- a/application/src/main/java/de/cotto/lndmanagej/InfoLogger.java +++ b/application/src/main/java/de/cotto/lndmanagej/InfoLogger.java @@ -15,7 +15,7 @@ public class InfoLogger { this.grpcGetInfo = grpcGetInfo; } - @Scheduled(fixedRate = 10_000) + @Scheduled(fixedRate = 60_000) public void logDetails() { logger.info("Alias: {}", grpcGetInfo.getAlias()); logger.info("Pubkey: {}", grpcGetInfo.getPubkey()); diff --git a/application/src/main/resources/application.properties b/application/src/main/resources/application.properties index 24298fcc..0ae260e8 100644 --- a/application/src/main/resources/application.properties +++ b/application/src/main/resources/application.properties @@ -3,6 +3,7 @@ spring.main.banner-mode=off spring.profiles.active=default logging.level.root=info logging.pattern.console=%d %clr(%-5p) %logger: %m%rEx{2}%n +spring.task.scheduling.pool.size=20 lndmanagej.macaroon-file=${user.home}/.lnd/data/chain/bitcoin/mainnet/admin.macaroon lndmanagej.cert-file=${user.home}/.lnd/tls.cert diff --git a/application/src/test/java/de/cotto/lndmanagej/HtlcLoggerTest.java b/application/src/test/java/de/cotto/lndmanagej/HtlcLoggerTest.java new file mode 100644 index 00000000..8973c835 --- /dev/null +++ b/application/src/test/java/de/cotto/lndmanagej/HtlcLoggerTest.java @@ -0,0 +1,44 @@ +package de.cotto.lndmanagej; + +import de.cotto.lndmanagej.grpc.GrpcHtlcEvents; +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 uk.org.lidalia.slf4jtest.TestLogger; +import uk.org.lidalia.slf4jtest.TestLoggerFactory; + +import java.util.stream.Stream; + +import static de.cotto.lndmanagej.model.ForwardFailureFixtures.FORWARD_FAILURE; +import static de.cotto.lndmanagej.model.SettledForwardFixtures.SETTLED_FORWARD; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static uk.org.lidalia.slf4jtest.LoggingEvent.info; + +@ExtendWith(MockitoExtension.class) +class HtlcLoggerTest { + private final TestLogger logger = TestLoggerFactory.getTestLogger(HtlcLogger.class); + + @InjectMocks + private HtlcLogger htlcLogger; + + @Mock + @SuppressWarnings("unused") + private GrpcHtlcEvents grpcHtlcEvents; + + @Test + void logSettledForwards() { + when(grpcHtlcEvents.getSettledForwards()).thenReturn(Stream.of(SETTLED_FORWARD)); + htlcLogger.logSettledForwards(); + assertThat(logger.getLoggingEvents()).contains(info("Settled Forward: {}", SETTLED_FORWARD)); + } + + @Test + void logForwardFailures() { + when(grpcHtlcEvents.getForwardFailures()).thenReturn(Stream.of(FORWARD_FAILURE)); + htlcLogger.logForwardFailures(); + assertThat(logger.getLoggingEvents()).contains(info("Forward Failure: {}", FORWARD_FAILURE)); + } +} \ No newline at end of file diff --git a/application/src/test/java/de/cotto/lndmanagej/InfoLoggerTest.java b/application/src/test/java/de/cotto/lndmanagej/InfoLoggerTest.java index 2665725e..9b75dfeb 100644 --- a/application/src/test/java/de/cotto/lndmanagej/InfoLoggerTest.java +++ b/application/src/test/java/de/cotto/lndmanagej/InfoLoggerTest.java @@ -9,7 +9,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import uk.org.lidalia.slf4jtest.TestLogger; import uk.org.lidalia.slf4jtest.TestLoggerFactory; -import static de.cotto.lndmanagej.graph.model.NodeFixtures.ALIAS; +import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; import static uk.org.lidalia.slf4jtest.LoggingEvent.info; diff --git a/graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/ChannelFixtures.java b/graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/ChannelFixtures.java deleted file mode 100644 index d2c603c5..00000000 --- a/graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/ChannelFixtures.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.cotto.lndmanagej.graph.model; - -import static de.cotto.lndmanagej.graph.model.ChannelIdFixtures.CHANNEL_ID; -import static de.cotto.lndmanagej.graph.model.NodeFixtures.NODE; -import static de.cotto.lndmanagej.graph.model.NodeFixtures.NODE_2; - -public class ChannelFixtures { - public static final Coins CAPACITY = Coins.ofSatoshis(21_000_000L); - - public static final Channel CHANNEL = Channel.builder() - .withChannelId(CHANNEL_ID) - .withCapacity(CAPACITY) - .withNode1(NODE) - .withNode2(NODE_2) - .build(); -} diff --git a/graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/ChannelIdFixtures.java b/graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/ChannelIdFixtures.java deleted file mode 100644 index 6e64c135..00000000 --- a/graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/ChannelIdFixtures.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.cotto.lndmanagej.graph.model; - -public class ChannelIdFixtures { - public static final ChannelId CHANNEL_ID = ChannelId.fromCompactForm("712345:123:1"); -} diff --git a/grpc-adapter/build.gradle b/grpc-adapter/build.gradle index de3b9ca4..966c2eeb 100644 --- a/grpc-adapter/build.gradle +++ b/grpc-adapter/build.gradle @@ -4,6 +4,8 @@ plugins { dependencies { implementation project(':grpc-client') + implementation project(':model') + testImplementation testFixtures(project(':model')) } jacocoTestCoverageVerification { diff --git a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcHtlcEvents.java b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcHtlcEvents.java new file mode 100644 index 00000000..854cc732 --- /dev/null +++ b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcHtlcEvents.java @@ -0,0 +1,109 @@ +package de.cotto.lndmanagej.grpc; + +import de.cotto.lndmanagej.model.ForwardAttempt; +import de.cotto.lndmanagej.model.ForwardFailure; +import de.cotto.lndmanagej.model.HtlcDetails; +import de.cotto.lndmanagej.model.SettledForward; +import org.springframework.stereotype.Component; +import routerrpc.RouterOuterClass; +import routerrpc.RouterOuterClass.HtlcEvent; + +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +@Component +public class GrpcHtlcEvents { + private final GrpcService grpcService; + private final Map previousAttempts; + + public GrpcHtlcEvents(GrpcService grpcService) { + this.grpcService = grpcService; + previousAttempts = new ConcurrentHashMap<>(); + } + + public Stream getForwardFailures() { + return getEventStream() + .filter(this::isForwardFailure) + .map(this::createForwardFailure) + .flatMap(Optional::stream); + } + + public Stream getSettledForwards() { + return getEventStream() + .filter(this::isSettledForward) + .map(this::createSettledForward) + .flatMap(Optional::stream); + } + + private Stream getEventStream() { + return Stream.iterate(grpcService.getHtlcEvents(), Iterator::hasNext, UnaryOperator.identity()) + .map(Iterator::next) + .map(this::storeAttempt); + } + + private HtlcEvent storeAttempt(HtlcEvent htlcEvent) { + if (isForwardAttempt(htlcEvent)) { + previousAttempts.put(getHtlcDetails(htlcEvent).withoutTimestamp(), createForwardAttempt(htlcEvent)); + } + return htlcEvent; + } + + private boolean isForwardAttempt(HtlcEvent htlcEvent) { + return htlcEvent.hasForwardEvent() + && htlcEvent.getIncomingChannelId() > 0 + && htlcEvent.getOutgoingChannelId() > 0; + } + + private boolean isForwardFailure(HtlcEvent htlcEvent) { + return htlcEvent.hasForwardFailEvent() + && htlcEvent.getIncomingChannelId() > 0 + && htlcEvent.getOutgoingChannelId() > 0; + } + + private boolean isSettledForward(HtlcEvent htlcEvent) { + return htlcEvent.hasSettleEvent() + && htlcEvent.getIncomingChannelId() > 0 + && htlcEvent.getOutgoingChannelId() > 0; + } + + private ForwardAttempt createForwardAttempt(HtlcEvent event) { + RouterOuterClass.HtlcInfo info = event.getForwardEvent().getInfo(); + return ForwardAttempt.builder() + .withHtlcDetails(getHtlcDetails(event)) + .withIncomingTimelock(info.getIncomingTimelock()) + .withOutgoingTimelock(info.getOutgoingTimelock()) + .withIncomingAmount(info.getIncomingAmtMsat()) + .withOutgoingAmount(info.getOutgoingAmtMsat()) + .build(); + } + + private Optional createForwardFailure(HtlcEvent event) { + HtlcDetails htlcDetails = getHtlcDetails(event); + return getForwardAttemptFor(htlcDetails).map(attempt -> new ForwardFailure(htlcDetails, attempt)); + } + + private Optional createSettledForward(HtlcEvent event) { + HtlcDetails htlcDetails = getHtlcDetails(event); + return getForwardAttemptFor(htlcDetails).map(attempt -> new SettledForward(htlcDetails, attempt)); + } + + private Optional getForwardAttemptFor(HtlcDetails htlcDetails) { + ForwardAttempt forwardAttempt = previousAttempts.remove(htlcDetails.withoutTimestamp()); + return Optional.ofNullable(forwardAttempt); + } + + private HtlcDetails getHtlcDetails(HtlcEvent event) { + return HtlcDetails.builder() + .withTimestamp(event.getTimestampNs()) + .withIncomingChannelId(event.getIncomingChannelId()) + .withOutgoingChannelId(event.getOutgoingChannelId()) + .withIncomingHtlcId(event.getIncomingHtlcId()) + .withOutgoingHtlcId(event.getOutgoingHtlcId()) + .build(); + } + +} diff --git a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcService.java b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcService.java index e5e1af1f..cb2a90bf 100644 --- a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcService.java +++ b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcService.java @@ -7,17 +7,24 @@ import lnrpc.LightningGrpc; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import routerrpc.RouterGrpc; +import routerrpc.RouterOuterClass; +import routerrpc.RouterOuterClass.SubscribeHtlcEventsRequest; import javax.annotation.PreDestroy; import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; import java.util.Optional; +import java.util.function.Supplier; @Component public class GrpcService { private final Logger logger = LoggerFactory.getLogger(getClass()); - private final LightningGrpc.LightningBlockingStub lightningStub; private final StubCreator stubCreator; + private final LightningGrpc.LightningBlockingStub lightningStub; + private final RouterGrpc.RouterBlockingStub routerStub; public GrpcService(LndConfiguration lndConfiguration) throws IOException { stubCreator = new StubCreator( @@ -26,7 +33,8 @@ public class GrpcService { lndConfiguration.getPort(), lndConfiguration.getHost() ); - lightningStub = this.stubCreator.getLightningStub(); + lightningStub = stubCreator.getLightningStub(); + routerStub = stubCreator.getRouterStub(); } @PreDestroy @@ -35,8 +43,17 @@ public class GrpcService { } Optional getInfo() { + return get(() -> lightningStub.getInfo(lnrpc.GetInfoRequest.getDefaultInstance())); + } + + Iterator getHtlcEvents() { + return get(() -> routerStub.subscribeHtlcEvents(SubscribeHtlcEventsRequest.newBuilder().build())) + .orElse(Collections.emptyIterator()); + } + + private Optional get(Supplier supplier) { try { - return Optional.of(lightningStub.getInfo(lnrpc.GetInfoRequest.getDefaultInstance())); + return Optional.ofNullable(supplier.get()); } catch (StatusRuntimeException exception) { logger.warn("Exception while connecting to lnd: ", exception); return Optional.empty(); diff --git a/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcHtlcEventsTest.java b/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcHtlcEventsTest.java new file mode 100644 index 00000000..07845046 --- /dev/null +++ b/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcHtlcEventsTest.java @@ -0,0 +1,207 @@ +package de.cotto.lndmanagej.grpc; + +import org.junit.jupiter.api.Nested; +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 routerrpc.RouterOuterClass.ForwardEvent; +import routerrpc.RouterOuterClass.ForwardFailEvent; +import routerrpc.RouterOuterClass.HtlcEvent; +import routerrpc.RouterOuterClass.SettleEvent; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_2; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3; +import static de.cotto.lndmanagej.model.ForwardFailureFixtures.FORWARD_FAILURE; +import static de.cotto.lndmanagej.model.SettledForwardFixtures.SETTLED_FORWARD; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GrpcHtlcEventsTest { + @InjectMocks + private GrpcHtlcEvents grpcHtlcEvents; + + @Mock + @SuppressWarnings("unused") + private GrpcService grpcService; + + private final HtlcEvent attemptEvent = getAttempt(CHANNEL_ID.getShortChannelId(), CHANNEL_ID_2.getShortChannelId()); + + @Nested + class GetForwardFailures { + + private final HtlcEvent failEvent = getFailEvent( + CHANNEL_ID.getShortChannelId(), + CHANNEL_ID_2.getShortChannelId() + ); + + @Test + void empty() { + when(grpcService.getHtlcEvents()).thenReturn(Collections.emptyIterator()); + assertThat(grpcHtlcEvents.getForwardFailures()).isEmpty(); + } + + @Test + void with_other_event() { + when(grpcService.getHtlcEvents()).thenReturn(Set.of(HtlcEvent.getDefaultInstance()).iterator()); + assertThat(grpcHtlcEvents.getForwardFailures()).isEmpty(); + } + + @Test + void with_fail_event() { + when(grpcService.getHtlcEvents()).thenReturn(Set.of(failEvent).iterator()); + assertThat(grpcHtlcEvents.getForwardFailures()).isEmpty(); + } + + @Test + void with_attempt_and_fail_event() { + when(grpcService.getHtlcEvents()).thenReturn(List.of(attemptEvent, failEvent).iterator()); + assertThat(grpcHtlcEvents.getForwardFailures()).containsExactly(FORWARD_FAILURE); + } + + @Test + void attempt_is_deleted_when_used() { + when(grpcService.getHtlcEvents()).thenReturn(List.of(attemptEvent, failEvent, failEvent).iterator()); + assertThat(grpcHtlcEvents.getForwardFailures()).hasSize(1); + } + + @Test + void with_other_attempt_and_fail_event() { + HtlcEvent attemptEvent = getAttempt(CHANNEL_ID.getShortChannelId(), CHANNEL_ID_3.getShortChannelId()); + when(grpcService.getHtlcEvents()).thenReturn(List.of(attemptEvent, failEvent).iterator()); + assertThat(grpcHtlcEvents.getForwardFailures()).isEmpty(); + } + + @Test + void ignores_failure_event_with_zero_incoming_channel_id() { + HtlcEvent failEvent = getFailEvent(0, CHANNEL_ID_2.getShortChannelId()); + when(grpcService.getHtlcEvents()).thenReturn(List.of(failEvent).iterator()); + assertThat(grpcHtlcEvents.getForwardFailures()).isEmpty(); + } + + @Test + void ignores_failure_event_with_zero_outgoing_channel_id() { + HtlcEvent failEvent = getFailEvent(CHANNEL_ID.getShortChannelId(), 0); + when(grpcService.getHtlcEvents()).thenReturn(List.of(failEvent).iterator()); + assertThat(grpcHtlcEvents.getForwardFailures()).isEmpty(); + } + + private HtlcEvent getFailEvent(long incomingChannelId, long outgoingChannelId) { + ForwardFailEvent forwardFailEvent = ForwardFailEvent.getDefaultInstance(); + return getBuilderWithDefaults(incomingChannelId, outgoingChannelId) + .setForwardFailEvent(forwardFailEvent) + .build(); + } + } + + @Nested + class GetSettledForwards { + + private final HtlcEvent settleEvent = getSettleEvent( + CHANNEL_ID.getShortChannelId(), + CHANNEL_ID_2.getShortChannelId() + ); + + @Test + void empty() { + when(grpcService.getHtlcEvents()).thenReturn(Collections.emptyIterator()); + assertThat(grpcHtlcEvents.getSettledForwards()).isEmpty(); + } + + @Test + void with_other_event() { + when(grpcService.getHtlcEvents()).thenReturn(Set.of(HtlcEvent.getDefaultInstance()).iterator()); + assertThat(grpcHtlcEvents.getSettledForwards()).isEmpty(); + } + + @Test + void with_settle_event() { + when(grpcService.getHtlcEvents()).thenReturn(Set.of(settleEvent).iterator()); + assertThat(grpcHtlcEvents.getSettledForwards()).isEmpty(); + } + + @Test + void with_attempt_and_settle_event() { + when(grpcService.getHtlcEvents()).thenReturn(List.of(attemptEvent, settleEvent).iterator()); + assertThat(grpcHtlcEvents.getSettledForwards()).containsExactly(SETTLED_FORWARD); + } + + @Test + void attempt_is_deleted_when_used() { + when(grpcService.getHtlcEvents()).thenReturn(List.of(attemptEvent, settleEvent, settleEvent).iterator()); + assertThat(grpcHtlcEvents.getSettledForwards()).hasSize(1); + } + + @Test + void with_other_attempt_and_settle_event() { + HtlcEvent attemptEvent = getAttempt(CHANNEL_ID.getShortChannelId(), CHANNEL_ID_3.getShortChannelId()); + when(grpcService.getHtlcEvents()).thenReturn(List.of(attemptEvent, settleEvent).iterator()); + assertThat(grpcHtlcEvents.getSettledForwards()).isEmpty(); + } + + @Test + void ignores_attempt_with_zero_incoming_channel_id() { + HtlcEvent attemptEvent = getAttempt(0, CHANNEL_ID_2.getShortChannelId()); + when(grpcService.getHtlcEvents()).thenReturn(List.of(attemptEvent).iterator()); + assertThat(grpcHtlcEvents.getSettledForwards()).isEmpty(); + } + + @Test + void ignores_attempt_with_zero_outgoing_channel_id() { + HtlcEvent attemptEvent = getAttempt(CHANNEL_ID.getShortChannelId(), 0); + when(grpcService.getHtlcEvents()).thenReturn(List.of(attemptEvent).iterator()); + assertThat(grpcHtlcEvents.getSettledForwards()).isEmpty(); + } + + @Test + void ignores_settle_event_with_zero_incoming_channel_id() { + HtlcEvent settleEvent = getSettleEvent(0, CHANNEL_ID_2.getShortChannelId()); + when(grpcService.getHtlcEvents()).thenReturn(List.of(settleEvent).iterator()); + assertThat(grpcHtlcEvents.getSettledForwards()).isEmpty(); + } + + @Test + void ignores_settle_event_with_zero_outgoing_channel_id() { + HtlcEvent settleEvent = getSettleEvent(CHANNEL_ID.getShortChannelId(), 0); + when(grpcService.getHtlcEvents()).thenReturn(List.of(settleEvent).iterator()); + assertThat(grpcHtlcEvents.getSettledForwards()).isEmpty(); + } + + private HtlcEvent getSettleEvent(long incomingChannelId, long outgoingChannelId) { + SettleEvent settleEvent = SettleEvent.getDefaultInstance(); + return getBuilderWithDefaults(incomingChannelId, outgoingChannelId) + .setSettleEvent(settleEvent) + .build(); + } + } + + private HtlcEvent getAttempt(long incomingChannelId, long outgoingChannelId) { + return getBuilderWithDefaults(incomingChannelId, outgoingChannelId) + .setForwardEvent(ForwardEvent.newBuilder() + .setInfo(RouterOuterClass.HtlcInfo.newBuilder() + .setIncomingTimelock(1) + .setOutgoingTimelock(2) + .setIncomingAmtMsat(100) + .setOutgoingAmtMsat(200) + .build()) + .build()) + .build(); + } + + private HtlcEvent.Builder getBuilderWithDefaults(long incomingChannelId, long outgoingChannelId) { + return HtlcEvent.newBuilder() + .setIncomingHtlcId(1) + .setOutgoingHtlcId(2) + .setTimestampNs(789) + .setIncomingChannelId(incomingChannelId) + .setOutgoingChannelId(outgoingChannelId); + } +} \ No newline at end of file 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 91d9ca55..8a21f75c 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 @@ -5,6 +5,7 @@ import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.NettyChannelBuilder; import io.netty.handler.ssl.SslContext; import lnrpc.LightningGrpc; +import routerrpc.RouterGrpc; import javax.net.ssl.SSLException; import java.io.File; @@ -14,6 +15,7 @@ public class StubCreator { private static final int FIFTY_MEGA_BYTE = 50 * 1024 * 1024; private final LightningGrpc.LightningBlockingStub stub; + private final RouterGrpc.RouterBlockingStub routerStub; private final ManagedChannel channel; private final File macaroonFile; private final File certFile; @@ -27,12 +29,17 @@ public class StubCreator { this.host = host; channel = getChannel(); stub = createLightningStub(); + routerStub = createRouterStub(); } public LightningGrpc.LightningBlockingStub getLightningStub() { return stub; } + public RouterGrpc.RouterBlockingStub getRouterStub() { + return routerStub; + } + public void shutdown() { channel.shutdown(); } @@ -48,4 +55,11 @@ public class StubCreator { .withMaxInboundMessageSize(FIFTY_MEGA_BYTE) .withCallCredentials(new MacaroonCallCredential(macaroonFile)); } + + private RouterGrpc.RouterBlockingStub createRouterStub() throws IOException { + return RouterGrpc + .newBlockingStub(channel) + .withMaxInboundMessageSize(FIFTY_MEGA_BYTE) + .withCallCredentials(new MacaroonCallCredential(macaroonFile)); + } } diff --git a/grpc-client/src/main/proto/router.proto b/grpc-client/src/main/proto/router.proto new file mode 100644 index 00000000..79becfdf --- /dev/null +++ b/grpc-client/src/main/proto/router.proto @@ -0,0 +1,800 @@ +syntax = "proto3"; + +import "lightning.proto"; + +package routerrpc; + +option go_package = "github.com/lightningnetwork/lnd/lnrpc/routerrpc"; + +// Router is a service that offers advanced interaction with the router +// subsystem of the daemon. +service Router { + /* + SendPaymentV2 attempts to route a payment described by the passed + PaymentRequest to the final destination. The call returns a stream of + payment updates. + */ + rpc SendPaymentV2 (SendPaymentRequest) returns (stream lnrpc.Payment); + + /* + TrackPaymentV2 returns an update stream for the payment identified by the + payment hash. + */ + rpc TrackPaymentV2 (TrackPaymentRequest) returns (stream lnrpc.Payment); + + /* + EstimateRouteFee allows callers to obtain a lower bound w.r.t how much it + may cost to send an HTLC to the target end destination. + */ + rpc EstimateRouteFee (RouteFeeRequest) returns (RouteFeeResponse); + + /* + Deprecated, use SendToRouteV2. SendToRoute attempts to make a payment via + the specified route. This method differs from SendPayment in that it + allows users to specify a full route manually. This can be used for + things like rebalancing, and atomic swaps. It differs from the newer + SendToRouteV2 in that it doesn't return the full HTLC information. + */ + rpc SendToRoute (SendToRouteRequest) returns (SendToRouteResponse) { + option deprecated = true; + } + + /* + SendToRouteV2 attempts to make a payment via the specified route. This + method differs from SendPayment in that it allows users to specify a full + route manually. This can be used for things like rebalancing, and atomic + swaps. + */ + rpc SendToRouteV2 (SendToRouteRequest) returns (lnrpc.HTLCAttempt); + + /* + ResetMissionControl clears all mission control state and starts with a clean + slate. + */ + rpc ResetMissionControl (ResetMissionControlRequest) + returns (ResetMissionControlResponse); + + /* + QueryMissionControl exposes the internal mission control state to callers. + It is a development feature. + */ + rpc QueryMissionControl (QueryMissionControlRequest) + returns (QueryMissionControlResponse); + + /* + XImportMissionControl is an experimental API that imports the state provided + to the internal mission control's state, using all results which are more + recent than our existing values. These values will only be imported + in-memory, and will not be persisted across restarts. + */ + rpc XImportMissionControl (XImportMissionControlRequest) + returns (XImportMissionControlResponse); + + /* + GetMissionControlConfig returns mission control's current config. + */ + rpc GetMissionControlConfig (GetMissionControlConfigRequest) + returns (GetMissionControlConfigResponse); + + /* + SetMissionControlConfig will set mission control's config, if the config + provided is valid. + */ + rpc SetMissionControlConfig (SetMissionControlConfigRequest) + returns (SetMissionControlConfigResponse); + + /* + QueryProbability returns the current success probability estimate for a + given node pair and amount. + */ + rpc QueryProbability (QueryProbabilityRequest) + returns (QueryProbabilityResponse); + + /* + BuildRoute builds a fully specified route based on a list of hop public + keys. It retrieves the relevant channel policies from the graph in order to + calculate the correct fees and time locks. + */ + rpc BuildRoute (BuildRouteRequest) returns (BuildRouteResponse); + + /* + SubscribeHtlcEvents creates a uni-directional stream from the server to + the client which delivers a stream of htlc events. + */ + rpc SubscribeHtlcEvents (SubscribeHtlcEventsRequest) + returns (stream HtlcEvent); + + /* + Deprecated, use SendPaymentV2. SendPayment attempts to route a payment + described by the passed PaymentRequest to the final destination. The call + returns a stream of payment status updates. + */ + rpc SendPayment (SendPaymentRequest) returns (stream PaymentStatus) { + option deprecated = true; + } + + /* + Deprecated, use TrackPaymentV2. TrackPayment returns an update stream for + the payment identified by the payment hash. + */ + rpc TrackPayment (TrackPaymentRequest) returns (stream PaymentStatus) { + option deprecated = true; + } + + /** + HtlcInterceptor dispatches a bi-directional streaming RPC in which + Forwarded HTLC requests are sent to the client and the client responds with + a boolean that tells LND if this htlc should be intercepted. + In case of interception, the htlc can be either settled, cancelled or + resumed later by using the ResolveHoldForward endpoint. + */ + rpc HtlcInterceptor (stream ForwardHtlcInterceptResponse) + returns (stream ForwardHtlcInterceptRequest); + + /* + UpdateChanStatus attempts to manually set the state of a channel + (enabled, disabled, or auto). A manual "disable" request will cause the + channel to stay disabled until a subsequent manual request of either + "enable" or "auto". + */ + rpc UpdateChanStatus (UpdateChanStatusRequest) + returns (UpdateChanStatusResponse); +} + +message SendPaymentRequest { + // The identity pubkey of the payment recipient + bytes dest = 1; + + /* + Number of satoshis to send. + + The fields amt and amt_msat are mutually exclusive. + */ + int64 amt = 2; + + /* + Number of millisatoshis to send. + + The fields amt and amt_msat are mutually exclusive. + */ + int64 amt_msat = 12; + + // The hash to use within the payment's HTLC + bytes payment_hash = 3; + + /* + The CLTV delta from the current height that should be used to set the + timelock for the final hop. + */ + int32 final_cltv_delta = 4; + + // An optional payment addr to be included within the last hop of the route. + bytes payment_addr = 20; + + /* + A bare-bones invoice for a payment within the Lightning Network. With the + details of the invoice, the sender has all the data necessary to send a + payment to the recipient. The amount in the payment request may be zero. In + that case it is required to set the amt field as well. If no payment request + is specified, the following fields are required: dest, amt and payment_hash. + */ + string payment_request = 5; + + /* + An upper limit on the amount of time we should spend when attempting to + fulfill the payment. This is expressed in seconds. If we cannot make a + successful payment within this time frame, an error will be returned. + This field must be non-zero. + */ + int32 timeout_seconds = 6; + + /* + The maximum number of satoshis that will be paid as a fee of the payment. + If this field is left to the default value of 0, only zero-fee routes will + be considered. This usually means single hop routes connecting directly to + the destination. To send the payment without a fee limit, use max int here. + + The fields fee_limit_sat and fee_limit_msat are mutually exclusive. + */ + int64 fee_limit_sat = 7; + + /* + The maximum number of millisatoshis that will be paid as a fee of the + payment. If this field is left to the default value of 0, only zero-fee + routes will be considered. This usually means single hop routes connecting + directly to the destination. To send the payment without a fee limit, use + max int here. + + The fields fee_limit_sat and fee_limit_msat are mutually exclusive. + */ + int64 fee_limit_msat = 13; + + /* + Deprecated, use outgoing_chan_ids. The channel id of the channel that must + be taken to the first hop. If zero, any channel may be used (unless + outgoing_chan_ids are set). + */ + uint64 outgoing_chan_id = 8 [jstype = JS_STRING, deprecated = true]; + + /* + The channel ids of the channels are allowed for the first hop. If empty, + any channel may be used. + */ + repeated uint64 outgoing_chan_ids = 19; + + /* + The pubkey of the last hop of the route. If empty, any hop may be used. + */ + bytes last_hop_pubkey = 14; + + /* + An optional maximum total time lock for the route. This should not exceed + lnd's `--max-cltv-expiry` setting. If zero, then the value of + `--max-cltv-expiry` is enforced. + */ + int32 cltv_limit = 9; + + /* + Optional route hints to reach the destination through private channels. + */ + repeated lnrpc.RouteHint route_hints = 10; + + /* + An optional field that can be used to pass an arbitrary set of TLV records + to a peer which understands the new records. This can be used to pass + application specific data during the payment attempt. Record types are + required to be in the custom range >= 65536. When using REST, the values + must be encoded as base64. + */ + map dest_custom_records = 11; + + // If set, circular payments to self are permitted. + bool allow_self_payment = 15; + + /* + Features assumed to be supported by the final node. All transitive feature + dependencies must also be set properly. For a given feature bit pair, either + optional or remote may be set, but not both. If this field is nil or empty, + the router will try to load destination features from the graph as a + fallback. + */ + repeated lnrpc.FeatureBit dest_features = 16; + + /* + The maximum number of partial payments that may be use to complete the full + amount. + */ + uint32 max_parts = 17; + + /* + If set, only the final payment update is streamed back. Intermediate updates + that show which htlcs are still in flight are suppressed. + */ + bool no_inflight_updates = 18; + + /* + The largest payment split that should be attempted when making a payment if + splitting is necessary. Setting this value will effectively cause lnd to + split more aggressively, vs only when it thinks it needs to. Note that this + value is in milli-satoshis. + */ + uint64 max_shard_size_msat = 21; + + /* + If set, an AMP-payment will be attempted. + */ + bool amp = 22; +} + +message TrackPaymentRequest { + // The hash of the payment to look up. + bytes payment_hash = 1; + + /* + If set, only the final payment update is streamed back. Intermediate updates + that show which htlcs are still in flight are suppressed. + */ + bool no_inflight_updates = 2; +} + +message RouteFeeRequest { + /* + The destination once wishes to obtain a routing fee quote to. + */ + bytes dest = 1; + + /* + The amount one wishes to send to the target destination. + */ + int64 amt_sat = 2; +} + +message RouteFeeResponse { + /* + A lower bound of the estimated fee to the target destination within the + network, expressed in milli-satoshis. + */ + int64 routing_fee_msat = 1; + + /* + An estimate of the worst case time delay that can occur. Note that callers + will still need to factor in the final CLTV delta of the last hop into this + value. + */ + int64 time_lock_delay = 2; +} + +message SendToRouteRequest { + // The payment hash to use for the HTLC. + bytes payment_hash = 1; + + // Route that should be used to attempt to complete the payment. + lnrpc.Route route = 2; +} + +message SendToRouteResponse { + // The preimage obtained by making the payment. + bytes preimage = 1; + + // The failure message in case the payment failed. + lnrpc.Failure failure = 2; +} + +message ResetMissionControlRequest { +} + +message ResetMissionControlResponse { +} + +message QueryMissionControlRequest { +} + +// QueryMissionControlResponse contains mission control state. +message QueryMissionControlResponse { + reserved 1; + + // Node pair-level mission control state. + repeated PairHistory pairs = 2; +} + +message XImportMissionControlRequest { + // Node pair-level mission control state to be imported. + repeated PairHistory pairs = 1; +} + +message XImportMissionControlResponse { +} + +// PairHistory contains the mission control state for a particular node pair. +message PairHistory { + // The source node pubkey of the pair. + bytes node_from = 1; + + // The destination node pubkey of the pair. + bytes node_to = 2; + + reserved 3, 4, 5, 6; + + PairData history = 7; +} + +message PairData { + // Time of last failure. + int64 fail_time = 1; + + /* + Lowest amount that failed to forward rounded to whole sats. This may be + set to zero if the failure is independent of amount. + */ + int64 fail_amt_sat = 2; + + /* + Lowest amount that failed to forward in millisats. This may be + set to zero if the failure is independent of amount. + */ + int64 fail_amt_msat = 4; + + reserved 3; + + // Time of last success. + int64 success_time = 5; + + // Highest amount that we could successfully forward rounded to whole sats. + int64 success_amt_sat = 6; + + // Highest amount that we could successfully forward in millisats. + int64 success_amt_msat = 7; +} + +message GetMissionControlConfigRequest { +} + +message GetMissionControlConfigResponse { + /* + Mission control's currently active config. + */ + MissionControlConfig config = 1; +} + +message SetMissionControlConfigRequest { + /* + The config to set for mission control. Note that all values *must* be set, + because the full config will be applied. + */ + MissionControlConfig config = 1; +} + +message SetMissionControlConfigResponse { +} + +message MissionControlConfig { + /* + The amount of time mission control will take to restore a penalized node + or channel back to 50% success probability, expressed in seconds. Setting + this value to a higher value will penalize failures for longer, making + mission control less likely to route through nodes and channels that we + have previously recorded failures for. + */ + uint64 half_life_seconds = 1; + + /* + The probability of success mission control should assign to hop in a route + where it has no other information available. Higher values will make mission + control more willing to try hops that we have no information about, lower + values will discourage trying these hops. + */ + float hop_probability = 2; + + /* + The importance that mission control should place on historical results, + expressed as a value in [0;1]. Setting this value to 1 will ignore all + historical payments and just use the hop probability to assess the + probability of success for each hop. A zero value ignores hop probability + completely and relies entirely on historical results, unless none are + available. + */ + float weight = 3; + + /* + The maximum number of payment results that mission control will store. + */ + uint32 maximum_payment_results = 4; + + /* + The minimum time that must have passed since the previously recorded failure + before we raise the failure amount. + */ + uint64 minimum_failure_relax_interval = 5; +} + +message QueryProbabilityRequest { + // The source node pubkey of the pair. + bytes from_node = 1; + + // The destination node pubkey of the pair. + bytes to_node = 2; + + // The amount for which to calculate a probability. + int64 amt_msat = 3; +} + +message QueryProbabilityResponse { + // The success probability for the requested pair. + double probability = 1; + + // The historical data for the requested pair. + PairData history = 2; +} + +message BuildRouteRequest { + /* + The amount to send expressed in msat. If set to zero, the minimum routable + amount is used. + */ + int64 amt_msat = 1; + + /* + CLTV delta from the current height that should be used for the timelock + of the final hop + */ + int32 final_cltv_delta = 2; + + /* + The channel id of the channel that must be taken to the first hop. If zero, + any channel may be used. + */ + uint64 outgoing_chan_id = 3 [jstype = JS_STRING]; + + /* + A list of hops that defines the route. This does not include the source hop + pubkey. + */ + repeated bytes hop_pubkeys = 4; + + // An optional payment addr to be included within the last hop of the route. + bytes payment_addr = 5; +} + +message BuildRouteResponse { + /* + Fully specified route that can be used to execute the payment. + */ + lnrpc.Route route = 1; +} + +message SubscribeHtlcEventsRequest { +} + +/* +HtlcEvent contains the htlc event that was processed. These are served on a +best-effort basis; events are not persisted, delivery is not guaranteed +(in the event of a crash in the switch, forward events may be lost) and +some events may be replayed upon restart. Events consumed from this package +should be de-duplicated by the htlc's unique combination of incoming and +outgoing channel id and htlc id. [EXPERIMENTAL] +*/ +message HtlcEvent { + /* + The short channel id that the incoming htlc arrived at our node on. This + value is zero for sends. + */ + uint64 incoming_channel_id = 1; + + /* + The short channel id that the outgoing htlc left our node on. This value + is zero for receives. + */ + uint64 outgoing_channel_id = 2; + + /* + Incoming id is the index of the incoming htlc in the incoming channel. + This value is zero for sends. + */ + uint64 incoming_htlc_id = 3; + + /* + Outgoing id is the index of the outgoing htlc in the outgoing channel. + This value is zero for receives. + */ + uint64 outgoing_htlc_id = 4; + + /* + The time in unix nanoseconds that the event occurred. + */ + uint64 timestamp_ns = 5; + + enum EventType { + UNKNOWN = 0; + SEND = 1; + RECEIVE = 2; + FORWARD = 3; + } + + /* + The event type indicates whether the htlc was part of a send, receive or + forward. + */ + EventType event_type = 6; + + oneof event { + ForwardEvent forward_event = 7; + ForwardFailEvent forward_fail_event = 8; + SettleEvent settle_event = 9; + LinkFailEvent link_fail_event = 10; + } +} + +message HtlcInfo { + // The timelock on the incoming htlc. + uint32 incoming_timelock = 1; + + // The timelock on the outgoing htlc. + uint32 outgoing_timelock = 2; + + // The amount of the incoming htlc. + uint64 incoming_amt_msat = 3; + + // The amount of the outgoing htlc. + uint64 outgoing_amt_msat = 4; +} + +message ForwardEvent { + // Info contains details about the htlc that was forwarded. + HtlcInfo info = 1; +} + +message ForwardFailEvent { +} + +message SettleEvent { + // The revealed preimage. + bytes preimage = 1; +} + +message LinkFailEvent { + // Info contains details about the htlc that we failed. + HtlcInfo info = 1; + + // FailureCode is the BOLT error code for the failure. + lnrpc.Failure.FailureCode wire_failure = 2; + + /* + FailureDetail provides additional information about the reason for the + failure. This detail enriches the information provided by the wire message + and may be 'no detail' if the wire message requires no additional metadata. + */ + FailureDetail failure_detail = 3; + + // A string representation of the link failure. + string failure_string = 4; +} + +enum FailureDetail { + UNKNOWN = 0; + NO_DETAIL = 1; + ONION_DECODE = 2; + LINK_NOT_ELIGIBLE = 3; + ON_CHAIN_TIMEOUT = 4; + HTLC_EXCEEDS_MAX = 5; + INSUFFICIENT_BALANCE = 6; + INCOMPLETE_FORWARD = 7; + HTLC_ADD_FAILED = 8; + FORWARDS_DISABLED = 9; + INVOICE_CANCELED = 10; + INVOICE_UNDERPAID = 11; + INVOICE_EXPIRY_TOO_SOON = 12; + INVOICE_NOT_OPEN = 13; + MPP_INVOICE_TIMEOUT = 14; + ADDRESS_MISMATCH = 15; + SET_TOTAL_MISMATCH = 16; + SET_TOTAL_TOO_LOW = 17; + SET_OVERPAID = 18; + UNKNOWN_INVOICE = 19; + INVALID_KEYSEND = 20; + MPP_IN_PROGRESS = 21; + CIRCULAR_ROUTE = 22; +} + +enum PaymentState { + /* + Payment is still in flight. + */ + IN_FLIGHT = 0; + + /* + Payment completed successfully. + */ + SUCCEEDED = 1; + + /* + There are more routes to try, but the payment timeout was exceeded. + */ + FAILED_TIMEOUT = 2; + + /* + All possible routes were tried and failed permanently. Or were no + routes to the destination at all. + */ + FAILED_NO_ROUTE = 3; + + /* + A non-recoverable error has occured. + */ + FAILED_ERROR = 4; + + /* + Payment details incorrect (unknown hash, invalid amt or + invalid final cltv delta) + */ + FAILED_INCORRECT_PAYMENT_DETAILS = 5; + + /* + Insufficient local balance. + */ + FAILED_INSUFFICIENT_BALANCE = 6; +} + +message PaymentStatus { + // Current state the payment is in. + PaymentState state = 1; + + /* + The pre-image of the payment when state is SUCCEEDED. + */ + bytes preimage = 2; + + reserved 3; + + /* + The HTLCs made in attempt to settle the payment [EXPERIMENTAL]. + */ + repeated lnrpc.HTLCAttempt htlcs = 4; +} + +message CircuitKey { + /// The id of the channel that the is part of this circuit. + uint64 chan_id = 1; + + /// The index of the incoming htlc in the incoming channel. + uint64 htlc_id = 2; +} + +message ForwardHtlcInterceptRequest { + /* + The key of this forwarded htlc. It defines the incoming channel id and + the index in this channel. + */ + CircuitKey incoming_circuit_key = 1; + + // The incoming htlc amount. + uint64 incoming_amount_msat = 5; + + // The incoming htlc expiry. + uint32 incoming_expiry = 6; + + /* + The htlc payment hash. This value is not guaranteed to be unique per + request. + */ + bytes payment_hash = 2; + + // The requested outgoing channel id for this forwarded htlc. Because of + // non-strict forwarding, this isn't necessarily the channel over which the + // packet will be forwarded eventually. A different channel to the same peer + // may be selected as well. + uint64 outgoing_requested_chan_id = 7; + + // The outgoing htlc amount. + uint64 outgoing_amount_msat = 3; + + // The outgoing htlc expiry. + uint32 outgoing_expiry = 4; + + // Any custom records that were present in the payload. + map custom_records = 8; + + // The onion blob for the next hop + bytes onion_blob = 9; +} + +/** +ForwardHtlcInterceptResponse enables the caller to resolve a previously hold +forward. The caller can choose either to: +- `Resume`: Execute the default behavior (usually forward). +- `Reject`: Fail the htlc backwards. +- `Settle`: Settle this htlc with a given preimage. +*/ +message ForwardHtlcInterceptResponse { + /** + The key of this forwarded htlc. It defines the incoming channel id and + the index in this channel. + */ + CircuitKey incoming_circuit_key = 1; + + // The resolve action for this intercepted htlc. + ResolveHoldForwardAction action = 2; + + // The preimage in case the resolve action is Settle. + bytes preimage = 3; +} + +enum ResolveHoldForwardAction { + SETTLE = 0; + FAIL = 1; + RESUME = 2; +} + +message UpdateChanStatusRequest { + lnrpc.ChannelPoint chan_point = 1; + + ChanStatusAction action = 2; +} + +enum ChanStatusAction { + ENABLE = 0; + DISABLE = 1; + AUTO = 2; +} + +message UpdateChanStatusResponse { +} diff --git a/graph/build.gradle b/model/build.gradle similarity index 100% rename from graph/build.gradle rename to model/build.gradle diff --git a/graph/src/main/java/de/cotto/lndmanagej/graph/model/Channel.java b/model/src/main/java/de/cotto/lndmanagej/model/Channel.java similarity index 98% rename from graph/src/main/java/de/cotto/lndmanagej/graph/model/Channel.java rename to model/src/main/java/de/cotto/lndmanagej/model/Channel.java index 82b0ca46..bd3536f2 100644 --- a/graph/src/main/java/de/cotto/lndmanagej/graph/model/Channel.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/Channel.java @@ -1,4 +1,4 @@ -package de.cotto.lndmanagej.graph.model; +package de.cotto.lndmanagej.model; import org.springframework.lang.Nullable; diff --git a/graph/src/main/java/de/cotto/lndmanagej/graph/model/ChannelId.java b/model/src/main/java/de/cotto/lndmanagej/model/ChannelId.java similarity index 92% rename from graph/src/main/java/de/cotto/lndmanagej/graph/model/ChannelId.java rename to model/src/main/java/de/cotto/lndmanagej/model/ChannelId.java index b6f315be..b551ee65 100644 --- a/graph/src/main/java/de/cotto/lndmanagej/graph/model/ChannelId.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/ChannelId.java @@ -1,4 +1,4 @@ -package de.cotto.lndmanagej.graph.model; +package de.cotto.lndmanagej.model; import java.util.Objects; @@ -51,4 +51,9 @@ public final class ChannelId { public int hashCode() { return Objects.hash(shortChannelId); } + + @Override + public String toString() { + return String.valueOf(shortChannelId); + } } diff --git a/graph/src/main/java/de/cotto/lndmanagej/graph/model/Coins.java b/model/src/main/java/de/cotto/lndmanagej/model/Coins.java similarity index 98% rename from graph/src/main/java/de/cotto/lndmanagej/graph/model/Coins.java rename to model/src/main/java/de/cotto/lndmanagej/model/Coins.java index 9aa0375a..951c9951 100644 --- a/graph/src/main/java/de/cotto/lndmanagej/graph/model/Coins.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/Coins.java @@ -1,4 +1,4 @@ -package de.cotto.lndmanagej.graph.model; +package de.cotto.lndmanagej.model; import java.math.BigDecimal; import java.util.Locale; diff --git a/model/src/main/java/de/cotto/lndmanagej/model/ForwardAttempt.java b/model/src/main/java/de/cotto/lndmanagej/model/ForwardAttempt.java new file mode 100644 index 00000000..9482d1db --- /dev/null +++ b/model/src/main/java/de/cotto/lndmanagej/model/ForwardAttempt.java @@ -0,0 +1,112 @@ +package de.cotto.lndmanagej.model; + +import javax.annotation.Nullable; +import java.util.Objects; + +public class ForwardAttempt { + private final HtlcDetails htlcDetails; + private final int incomingTimelock; + private final int outgoingTimelock; + private final Coins incomingAmount; + private final Coins outgoingAmount; + + private ForwardAttempt( + HtlcDetails htlcDetails, + int incomingTimelock, + int outgoingTimelock, + Coins incomingAmount, + Coins outgoingAmount + ) { + this.htlcDetails = htlcDetails; + this.incomingTimelock = incomingTimelock; + this.outgoingTimelock = outgoingTimelock; + this.incomingAmount = incomingAmount; + this.outgoingAmount = outgoingAmount; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public String toString() { + return "ForwardAttempt{" + + "htlcDetails=" + htlcDetails + + ", incomingTimelock=" + incomingTimelock + + ", outgoingTimelock=" + outgoingTimelock + + ", incomingAmount=" + incomingAmount + + ", outgoingAmount=" + outgoingAmount + + '}'; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + ForwardAttempt that = (ForwardAttempt) other; + return incomingTimelock == that.incomingTimelock + && outgoingTimelock == that.outgoingTimelock + && Objects.equals(htlcDetails, that.htlcDetails) + && Objects.equals(incomingAmount, that.incomingAmount) + && Objects.equals(outgoingAmount, that.outgoingAmount); + } + + @Override + public int hashCode() { + return Objects.hash(htlcDetails, incomingTimelock, outgoingTimelock, incomingAmount, outgoingAmount); + } + + public static class Builder { + private int incomingTimelock; + + private int outgoingTimelock; + + @Nullable + private Coins incomingAmount; + + @Nullable + private Coins outgoingAmount; + + @Nullable + private HtlcDetails htlcDetails; + + public Builder withIncomingTimelock(int incomingTimelock) { + this.incomingTimelock = incomingTimelock; + return this; + } + + public Builder withOutgoingTimelock(int outgoingTimelock) { + this.outgoingTimelock = outgoingTimelock; + return this; + } + + public Builder withIncomingAmount(long incomingAmtMsat) { + this.incomingAmount = Coins.ofMilliSatoshis(incomingAmtMsat); + return this; + } + + public Builder withOutgoingAmount(long outgoingAmtMsat) { + this.outgoingAmount = Coins.ofMilliSatoshis(outgoingAmtMsat); + return this; + } + + public Builder withHtlcDetails(HtlcDetails htlcDetails) { + this.htlcDetails = htlcDetails; + return this; + } + + public ForwardAttempt build() { + return new ForwardAttempt( + Objects.requireNonNull(htlcDetails), + incomingTimelock, + outgoingTimelock, + Objects.requireNonNull(incomingAmount), + Objects.requireNonNull(outgoingAmount) + ); + } + } +} diff --git a/model/src/main/java/de/cotto/lndmanagej/model/ForwardFailure.java b/model/src/main/java/de/cotto/lndmanagej/model/ForwardFailure.java new file mode 100644 index 00000000..504b31e5 --- /dev/null +++ b/model/src/main/java/de/cotto/lndmanagej/model/ForwardFailure.java @@ -0,0 +1,38 @@ +package de.cotto.lndmanagej.model; + +import java.util.Objects; + +public class ForwardFailure { + private final HtlcDetails htlcDetails; + private final ForwardAttempt forwardAttempt; + + public ForwardFailure(HtlcDetails htlcDetails, ForwardAttempt forwardAttempt) { + this.htlcDetails = htlcDetails; + this.forwardAttempt = forwardAttempt; + } + + @Override + public String toString() { + return "ForwardFailure{" + + "htlcDetails=" + htlcDetails + + ", forwardAttempt=" + forwardAttempt + + '}'; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + ForwardFailure that = (ForwardFailure) other; + return Objects.equals(htlcDetails, that.htlcDetails) && Objects.equals(forwardAttempt, that.forwardAttempt); + } + + @Override + public int hashCode() { + return Objects.hash(htlcDetails, forwardAttempt); + } +} diff --git a/model/src/main/java/de/cotto/lndmanagej/model/HtlcDetails.java b/model/src/main/java/de/cotto/lndmanagej/model/HtlcDetails.java new file mode 100644 index 00000000..1d5764c3 --- /dev/null +++ b/model/src/main/java/de/cotto/lndmanagej/model/HtlcDetails.java @@ -0,0 +1,123 @@ +package de.cotto.lndmanagej.model; + +import javax.annotation.Nullable; +import java.time.Instant; +import java.util.Objects; + +public class HtlcDetails { + private final ChannelId incomingChannelId; + private final ChannelId outgoingChannelId; + private final long incomingHtlcId; + private final long outgoingHtlcId; + private final Instant timestamp; + + private HtlcDetails( + ChannelId incomingChannelId, + ChannelId outgoingChannelId, + long incomingHtlcId, + long outgoingHtlcId, + Instant timestamp + ) { + this.incomingChannelId = incomingChannelId; + this.outgoingChannelId = outgoingChannelId; + this.incomingHtlcId = incomingHtlcId; + this.outgoingHtlcId = outgoingHtlcId; + this.timestamp = timestamp; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public String toString() { + return "HtlcDetails{" + + "incomingChannelId=" + incomingChannelId + + ", outgoingChannelId=" + outgoingChannelId + + ", incomingHtlcId=" + incomingHtlcId + + ", outgoingHtlcId=" + outgoingHtlcId + + ", timestamp=" + timestamp + + '}'; + } + + public HtlcDetails withoutTimestamp() { + return new HtlcDetails( + incomingChannelId, outgoingChannelId, incomingHtlcId, outgoingHtlcId, Instant.ofEpochMilli(0) + ); + } + + public Instant getTimestamp() { + return timestamp; + } + + public static class Builder { + @Nullable + private ChannelId incomingChannelId; + + @Nullable + private ChannelId outgoingChannelId; + + private long incomingHtlcId; + + private long outgoingHtlcId; + + @Nullable + private Instant timestamp; + + public Builder withIncomingChannelId(long incomingChannelId) { + this.incomingChannelId = ChannelId.fromShortChannelId(incomingChannelId); + return this; + } + + public Builder withOutgoingChannelId(long outgoingChannelId) { + this.outgoingChannelId = ChannelId.fromShortChannelId(outgoingChannelId); + return this; + } + + public Builder withIncomingHtlcId(long incomingHtlcId) { + this.incomingHtlcId = incomingHtlcId; + return this; + } + + public Builder withOutgoingHtlcId(long outgoingHtlcId) { + this.outgoingHtlcId = outgoingHtlcId; + return this; + } + + public Builder withTimestamp(long timestampNs) { + this.timestamp = Instant.ofEpochSecond(0, timestampNs); + return this; + } + + public HtlcDetails build() { + return new HtlcDetails( + Objects.requireNonNull(incomingChannelId), + Objects.requireNonNull(outgoingChannelId), + incomingHtlcId, + outgoingHtlcId, + Objects.requireNonNull(timestamp) + ); + } + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + HtlcDetails that = (HtlcDetails) other; + return incomingHtlcId == that.incomingHtlcId + && outgoingHtlcId == that.outgoingHtlcId + && Objects.equals(incomingChannelId, that.incomingChannelId) + && Objects.equals(outgoingChannelId, that.outgoingChannelId) + && Objects.equals(timestamp, that.timestamp); + } + + @Override + public int hashCode() { + return Objects.hash(incomingChannelId, outgoingChannelId, incomingHtlcId, outgoingHtlcId, timestamp); + } +} diff --git a/graph/src/main/java/de/cotto/lndmanagej/graph/model/Node.java b/model/src/main/java/de/cotto/lndmanagej/model/Node.java similarity index 98% rename from graph/src/main/java/de/cotto/lndmanagej/graph/model/Node.java rename to model/src/main/java/de/cotto/lndmanagej/model/Node.java index c8eb33ab..10186ea6 100644 --- a/graph/src/main/java/de/cotto/lndmanagej/graph/model/Node.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/Node.java @@ -1,4 +1,4 @@ -package de.cotto.lndmanagej.graph.model; +package de.cotto.lndmanagej.model; import javax.annotation.Nullable; import java.util.Objects; diff --git a/model/src/main/java/de/cotto/lndmanagej/model/SettledForward.java b/model/src/main/java/de/cotto/lndmanagej/model/SettledForward.java new file mode 100644 index 00000000..ac91b925 --- /dev/null +++ b/model/src/main/java/de/cotto/lndmanagej/model/SettledForward.java @@ -0,0 +1,38 @@ +package de.cotto.lndmanagej.model; + +import java.util.Objects; + +public class SettledForward { + private final HtlcDetails htlcDetails; + private final ForwardAttempt attempt; + + public SettledForward(HtlcDetails htlcDetails, ForwardAttempt attempt) { + this.htlcDetails = htlcDetails; + this.attempt = attempt; + } + + @Override + public String toString() { + return "SettledForward{" + + "htlcDetails=" + htlcDetails + + ", attempt=" + attempt + + '}'; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + SettledForward that = (SettledForward) other; + return Objects.equals(htlcDetails, that.htlcDetails) && Objects.equals(attempt, that.attempt); + } + + @Override + public int hashCode() { + return Objects.hash(htlcDetails, attempt); + } +} diff --git a/graph/src/test/java/de/cotto/lndmanagej/graph/model/ChannelIdTest.java b/model/src/test/java/de/cotto/lndmanagej/model/ChannelIdTest.java similarity index 93% rename from graph/src/test/java/de/cotto/lndmanagej/graph/model/ChannelIdTest.java rename to model/src/test/java/de/cotto/lndmanagej/model/ChannelIdTest.java index 841d50ce..106da439 100644 --- a/graph/src/test/java/de/cotto/lndmanagej/graph/model/ChannelIdTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/ChannelIdTest.java @@ -1,4 +1,4 @@ -package de.cotto.lndmanagej.graph.model; +package de.cotto.lndmanagej.model; import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.jupiter.api.Nested; @@ -108,4 +108,10 @@ class ChannelIdTest { void testEquals() { EqualsVerifier.forClass(ChannelId.class).verify(); } + + @Test + void testToString() { + String expectedString = String.valueOf(ChannelIdFixtures.CHANNEL_ID.getShortChannelId()); + assertThat(ChannelIdFixtures.CHANNEL_ID).hasToString(expectedString); + } } \ No newline at end of file diff --git a/graph/src/test/java/de/cotto/lndmanagej/graph/model/ChannelTest.java b/model/src/test/java/de/cotto/lndmanagej/model/ChannelTest.java similarity index 78% rename from graph/src/test/java/de/cotto/lndmanagej/graph/model/ChannelTest.java rename to model/src/test/java/de/cotto/lndmanagej/model/ChannelTest.java index 4943a69a..8a8c63c1 100644 --- a/graph/src/test/java/de/cotto/lndmanagej/graph/model/ChannelTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/ChannelTest.java @@ -1,13 +1,12 @@ -package de.cotto.lndmanagej.graph.model; +package de.cotto.lndmanagej.model; import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.jupiter.api.Test; -import static de.cotto.lndmanagej.graph.model.ChannelFixtures.CAPACITY; -import static de.cotto.lndmanagej.graph.model.ChannelFixtures.CHANNEL; -import static de.cotto.lndmanagej.graph.model.ChannelIdFixtures.CHANNEL_ID; -import static de.cotto.lndmanagej.graph.model.NodeFixtures.NODE; -import static de.cotto.lndmanagej.graph.model.NodeFixtures.NODE_2; +import static de.cotto.lndmanagej.model.ChannelFixtures.CAPACITY; +import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL; +import static de.cotto.lndmanagej.model.NodeFixtures.NODE; +import static de.cotto.lndmanagej.model.NodeFixtures.NODE_2; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; @@ -34,7 +33,7 @@ class ChannelTest { void builder_without_capacity() { assertThatNullPointerException().isThrownBy( () -> Channel.builder() - .withChannelId(CHANNEL_ID) + .withChannelId(ChannelIdFixtures.CHANNEL_ID) .withNode1(NODE) .withNode2(NODE_2) .build() @@ -45,7 +44,7 @@ class ChannelTest { void builder_without_node1() { assertThatNullPointerException().isThrownBy( () -> Channel.builder() - .withChannelId(CHANNEL_ID) + .withChannelId(ChannelIdFixtures.CHANNEL_ID) .withCapacity(CAPACITY) .withNode2(NODE_2) .build() @@ -56,7 +55,7 @@ class ChannelTest { void builder_without_node2() { assertThatNullPointerException().isThrownBy( () -> Channel.builder() - .withChannelId(CHANNEL_ID) + .withChannelId(ChannelIdFixtures.CHANNEL_ID) .withCapacity(CAPACITY) .withNode1(NODE) .build() @@ -66,7 +65,7 @@ class ChannelTest { @Test void builder_with_all_arguments() { Channel channel = Channel.builder() - .withChannelId(CHANNEL_ID) + .withChannelId(ChannelIdFixtures.CHANNEL_ID) .withCapacity(CAPACITY) .withNode1(NODE) .withNode2(NODE_2) @@ -76,7 +75,7 @@ class ChannelTest { @Test void getId() { - assertThat(CHANNEL.getId()).isEqualTo(CHANNEL_ID); + assertThat(CHANNEL.getId()).isEqualTo(ChannelIdFixtures.CHANNEL_ID); } @Test @@ -97,7 +96,7 @@ class ChannelTest { @Test void testEquals_reversed_nodes() { Channel channel = Channel.builder() - .withChannelId(CHANNEL_ID) + .withChannelId(ChannelIdFixtures.CHANNEL_ID) .withCapacity(CAPACITY) .withNode1(NODE_2) .withNode2(NODE) diff --git a/graph/src/test/java/de/cotto/lndmanagej/graph/model/CoinsTest.java b/model/src/test/java/de/cotto/lndmanagej/model/CoinsTest.java similarity index 99% rename from graph/src/test/java/de/cotto/lndmanagej/graph/model/CoinsTest.java rename to model/src/test/java/de/cotto/lndmanagej/model/CoinsTest.java index 82dba1a3..2bb74844 100644 --- a/graph/src/test/java/de/cotto/lndmanagej/graph/model/CoinsTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/CoinsTest.java @@ -1,4 +1,4 @@ -package de.cotto.lndmanagej.graph.model; +package de.cotto.lndmanagej.model; import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.jupiter.api.Test; diff --git a/model/src/test/java/de/cotto/lndmanagej/model/ForwardAttemptTest.java b/model/src/test/java/de/cotto/lndmanagej/model/ForwardAttemptTest.java new file mode 100644 index 00000000..f40531d3 --- /dev/null +++ b/model/src/test/java/de/cotto/lndmanagej/model/ForwardAttemptTest.java @@ -0,0 +1,48 @@ +package de.cotto.lndmanagej.model; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +import static de.cotto.lndmanagej.model.ForwardAttemptFixtures.FORWARD_ATTEMPT; +import static de.cotto.lndmanagej.model.HtlcDetailsFixtures.HTLC_DETAILS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +class ForwardAttemptTest { + @Test + void builder_without_arguments() { + assertThatNullPointerException().isThrownBy(() -> + ForwardAttempt.builder().build() + ); + } + + @Test + void builder_with_all_arguments() { + ForwardAttempt forwardAttempt = ForwardAttempt.builder() + .withHtlcDetails(HTLC_DETAILS) + .withIncomingAmount(123) + .withOutgoingAmount(456) + .withIncomingTimelock(1) + .withOutgoingTimelock(2) + .build(); + assertThat(forwardAttempt).isNotNull(); + } + + @Test + void testToString() { + assertThat(FORWARD_ATTEMPT).hasToString( + "ForwardAttempt{" + + "htlcDetails=" + HTLC_DETAILS + + ", incomingTimelock=1" + + ", outgoingTimelock=2" + + ", incomingAmount=0.100" + + ", outgoingAmount=0.200" + + "}" + ); + } + + @Test + void testEquals() { + EqualsVerifier.forClass(ForwardAttempt.class).usingGetClass().verify(); + } +} \ No newline at end of file diff --git a/model/src/test/java/de/cotto/lndmanagej/model/ForwardFailureTest.java b/model/src/test/java/de/cotto/lndmanagej/model/ForwardFailureTest.java new file mode 100644 index 00000000..c348202f --- /dev/null +++ b/model/src/test/java/de/cotto/lndmanagej/model/ForwardFailureTest.java @@ -0,0 +1,31 @@ +package de.cotto.lndmanagej.model; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +import static de.cotto.lndmanagej.model.ForwardFailureFixtures.FORWARD_FAILURE; +import static de.cotto.lndmanagej.model.HtlcDetailsFixtures.HTLC_DETAILS; +import static org.assertj.core.api.Assertions.assertThat; + +class ForwardFailureTest { + @Test + void testToString() { + assertThat(FORWARD_FAILURE).hasToString( + "ForwardFailure{" + + "htlcDetails=" + HTLC_DETAILS + + ", forwardAttempt=ForwardAttempt{" + + "htlcDetails=" + HTLC_DETAILS + + ", incomingTimelock=1" + + ", outgoingTimelock=2" + + ", incomingAmount=0.100" + + ", outgoingAmount=0.200" + + "}" + + "}" + ); + } + + @Test + void testEquals() { + EqualsVerifier.forClass(ForwardFailure.class).usingGetClass().verify(); + } +} \ No newline at end of file diff --git a/model/src/test/java/de/cotto/lndmanagej/model/HtlcDetailsTest.java b/model/src/test/java/de/cotto/lndmanagej/model/HtlcDetailsTest.java new file mode 100644 index 00000000..90f353df --- /dev/null +++ b/model/src/test/java/de/cotto/lndmanagej/model/HtlcDetailsTest.java @@ -0,0 +1,61 @@ +package de.cotto.lndmanagej.model; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_2; +import static de.cotto.lndmanagej.model.HtlcDetailsFixtures.HTLC_DETAILS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +class HtlcDetailsTest { + @Test + void builder_without_arguments() { + assertThatNullPointerException().isThrownBy(() -> + HtlcDetails.builder().build() + ); + } + + @Test + void builder_with_all_arguments() { + HtlcDetails htlcDetails = HtlcDetails.builder() + .withIncomingChannelId(CHANNEL_ID.getShortChannelId()) + .withOutgoingChannelId(CHANNEL_ID_2.getShortChannelId()) + .withTimestamp(789) + .withIncomingHtlcId(1) + .withOutgoingHtlcId(2) + .build(); + assertThat(htlcDetails).isNotNull(); + } + + @Test + void getTimestamp() { + assertThat(HTLC_DETAILS.getTimestamp()).isEqualTo(Instant.ofEpochSecond(0, 789)); + } + + @Test + void withoutTimestamp() { + assertThat(HTLC_DETAILS.withoutTimestamp().getTimestamp()).isEqualTo(Instant.ofEpochSecond(0)); + } + + @Test + void testToString() { + assertThat(HTLC_DETAILS).hasToString( + "HtlcDetails{" + + "incomingChannelId=783231610496155649" + + ", outgoingChannelId=879608202739056642" + + ", incomingHtlcId=1" + + ", outgoingHtlcId=2" + + ", timestamp=1970-01-01T00:00:00.000000789Z" + + "}" + ); + } + + @Test + void testEquals() { + EqualsVerifier.forClass(HtlcDetails.class).usingGetClass().verify(); + } +} \ No newline at end of file diff --git a/graph/src/test/java/de/cotto/lndmanagej/graph/model/NodeTest.java b/model/src/test/java/de/cotto/lndmanagej/model/NodeTest.java similarity index 95% rename from graph/src/test/java/de/cotto/lndmanagej/graph/model/NodeTest.java rename to model/src/test/java/de/cotto/lndmanagej/model/NodeTest.java index 1bcff294..03f9fb6b 100644 --- a/graph/src/test/java/de/cotto/lndmanagej/graph/model/NodeTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/NodeTest.java @@ -1,10 +1,10 @@ -package de.cotto.lndmanagej.graph.model; +package de.cotto.lndmanagej.model; import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.jupiter.api.Test; -import static de.cotto.lndmanagej.graph.model.NodeFixtures.NODE; -import static de.cotto.lndmanagej.graph.model.NodeFixtures.NODE_WITHOUT_ALIAS; +import static de.cotto.lndmanagej.model.NodeFixtures.NODE; +import static de.cotto.lndmanagej.model.NodeFixtures.NODE_WITHOUT_ALIAS; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; diff --git a/model/src/test/java/de/cotto/lndmanagej/model/SettledForwardTest.java b/model/src/test/java/de/cotto/lndmanagej/model/SettledForwardTest.java new file mode 100644 index 00000000..a4ca3c36 --- /dev/null +++ b/model/src/test/java/de/cotto/lndmanagej/model/SettledForwardTest.java @@ -0,0 +1,26 @@ +package de.cotto.lndmanagej.model; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +import static de.cotto.lndmanagej.model.ForwardAttemptFixtures.FORWARD_ATTEMPT; +import static de.cotto.lndmanagej.model.HtlcDetailsFixtures.HTLC_DETAILS; +import static de.cotto.lndmanagej.model.SettledForwardFixtures.SETTLED_FORWARD; +import static org.assertj.core.api.Assertions.assertThat; + +class SettledForwardTest { + @Test + void testToString() { + assertThat(SETTLED_FORWARD).hasToString( + "SettledForward{" + + "htlcDetails=" + HTLC_DETAILS + + ", attempt=" + FORWARD_ATTEMPT + + "}" + ); + } + + @Test + void testEquals() { + EqualsVerifier.forClass(SettledForward.class).usingGetClass().verify(); + } +} \ No newline at end of file diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelFixtures.java new file mode 100644 index 00000000..9872139b --- /dev/null +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelFixtures.java @@ -0,0 +1,15 @@ +package de.cotto.lndmanagej.model; + +import static de.cotto.lndmanagej.model.NodeFixtures.NODE; +import static de.cotto.lndmanagej.model.NodeFixtures.NODE_2; + +public class ChannelFixtures { + public static final Coins CAPACITY = Coins.ofSatoshis(21_000_000L); + + public static final Channel CHANNEL = Channel.builder() + .withChannelId(ChannelIdFixtures.CHANNEL_ID) + .withCapacity(CAPACITY) + .withNode1(NODE) + .withNode2(NODE_2) + .build(); +} diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelIdFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelIdFixtures.java new file mode 100644 index 00000000..f88f1240 --- /dev/null +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ChannelIdFixtures.java @@ -0,0 +1,7 @@ +package de.cotto.lndmanagej.model; + +public class ChannelIdFixtures { + public static final ChannelId CHANNEL_ID = ChannelId.fromCompactForm("712345:123:1"); + public static final ChannelId CHANNEL_ID_2 = ChannelId.fromCompactForm("799999:456:2"); + public static final ChannelId CHANNEL_ID_3 = ChannelId.fromCompactForm("799999:456:3"); +} diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardAttemptFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardAttemptFixtures.java new file mode 100644 index 00000000..2ad206f5 --- /dev/null +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardAttemptFixtures.java @@ -0,0 +1,13 @@ +package de.cotto.lndmanagej.model; + +import static de.cotto.lndmanagej.model.HtlcDetailsFixtures.HTLC_DETAILS; + +public class ForwardAttemptFixtures { + public static final ForwardAttempt FORWARD_ATTEMPT = ForwardAttempt.builder() + .withIncomingTimelock(1) + .withOutgoingTimelock(2) + .withHtlcDetails(HTLC_DETAILS) + .withOutgoingAmount(200) + .withIncomingAmount(100) + .build(); +} diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardFailureFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardFailureFixtures.java new file mode 100644 index 00000000..97818c74 --- /dev/null +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardFailureFixtures.java @@ -0,0 +1,8 @@ +package de.cotto.lndmanagej.model; + +import static de.cotto.lndmanagej.model.ForwardAttemptFixtures.FORWARD_ATTEMPT; +import static de.cotto.lndmanagej.model.HtlcDetailsFixtures.HTLC_DETAILS; + +public class ForwardFailureFixtures { + public static final ForwardFailure FORWARD_FAILURE = new ForwardFailure(HTLC_DETAILS, FORWARD_ATTEMPT); +} diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/HtlcDetailsFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/HtlcDetailsFixtures.java new file mode 100644 index 00000000..1101fe7f --- /dev/null +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/HtlcDetailsFixtures.java @@ -0,0 +1,14 @@ +package de.cotto.lndmanagej.model; + +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_2; + +public class HtlcDetailsFixtures { + public static final HtlcDetails HTLC_DETAILS = HtlcDetails.builder() + .withIncomingChannelId(CHANNEL_ID.getShortChannelId()) + .withOutgoingChannelId(CHANNEL_ID_2.getShortChannelId()) + .withTimestamp(789) + .withIncomingHtlcId(1) + .withOutgoingHtlcId(2) + .build(); +} diff --git a/graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/NodeFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeFixtures.java similarity index 96% rename from graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/NodeFixtures.java rename to model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeFixtures.java index 9c9d9ff8..4f7e625b 100644 --- a/graph/src/testFixtures/java/de/cotto/lndmanagej/graph/model/NodeFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/NodeFixtures.java @@ -1,4 +1,4 @@ -package de.cotto.lndmanagej.graph.model; +package de.cotto.lndmanagej.model; import java.time.Instant; diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/SettledForwardFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/SettledForwardFixtures.java new file mode 100644 index 00000000..dab616e7 --- /dev/null +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/SettledForwardFixtures.java @@ -0,0 +1,8 @@ +package de.cotto.lndmanagej.model; + +import static de.cotto.lndmanagej.model.ForwardAttemptFixtures.FORWARD_ATTEMPT; +import static de.cotto.lndmanagej.model.HtlcDetailsFixtures.HTLC_DETAILS; + +public class SettledForwardFixtures { + public static final SettledForward SETTLED_FORWARD = new SettledForward(HTLC_DETAILS, FORWARD_ATTEMPT); +} diff --git a/settings.gradle b/settings.gradle index 69c8fc59..de09a685 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ rootProject.name = 'lnd-manageJ' include 'application' -include 'graph' +include 'model' include 'grpc-client' include 'grpc-adapter' \ No newline at end of file