From cf39bfd62b176285d4ffd2b853de23e269b37ff1 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sun, 13 Mar 2022 15:24:43 +0100 Subject: [PATCH] get LN graph via gRPC --- .../de/cotto/lndmanagej/grpc/GrpcGraph.java | 76 ++++++++++++ .../de/cotto/lndmanagej/grpc/GrpcService.java | 10 ++ .../cotto/lndmanagej/grpc/GrpcGraphTest.java | 113 ++++++++++++++++++ .../lndmanagej/model/DirectedChannelEdge.java | 4 + .../model/DirectedChannelEdgeTest.java | 38 ++++++ .../model/DirectedChannelEdgeFixtures.java | 17 +++ 6 files changed, 258 insertions(+) create mode 100644 grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcGraph.java create mode 100644 grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcGraphTest.java create mode 100644 model/src/main/java/de/cotto/lndmanagej/model/DirectedChannelEdge.java create mode 100644 model/src/test/java/de/cotto/lndmanagej/model/DirectedChannelEdgeTest.java create mode 100644 model/src/testFixtures/java/de/cotto/lndmanagej/model/DirectedChannelEdgeFixtures.java diff --git a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcGraph.java b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcGraph.java new file mode 100644 index 00000000..3afc309c --- /dev/null +++ b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcGraph.java @@ -0,0 +1,76 @@ +package de.cotto.lndmanagej.grpc; + +import com.github.benmanes.caffeine.cache.LoadingCache; +import de.cotto.lndmanagej.caching.CacheBuilder; +import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.DirectedChannelEdge; +import de.cotto.lndmanagej.model.Policy; +import de.cotto.lndmanagej.model.Pubkey; +import lnrpc.ChannelEdge; +import lnrpc.ChannelGraph; +import lnrpc.RoutingPolicy; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +@Component +public class GrpcGraph { + private final GrpcService grpcService; + private final LoadingCache>> channelEdgeCache; + + public GrpcGraph(GrpcService grpcService) { + this.grpcService = grpcService; + channelEdgeCache = new CacheBuilder() + .withExpiry(Duration.ofMinutes(2)) + .withRefresh(Duration.ofMinutes(1)) + .withSoftValues(true) + .build(this::getChannelEdgesWithoutCache); + } + + public Optional> getChannelEdges() { + return channelEdgeCache.get(""); + } + + public Optional> getChannelEdgesWithoutCache() { + ChannelGraph channelGraph = grpcService.describeGraph().orElse(null); + if (channelGraph == null) { + return Optional.empty(); + } + Set channelEdges = new LinkedHashSet<>(); + for (ChannelEdge channelEdge : channelGraph.getEdgesList()) { + ChannelId channelId = ChannelId.fromShortChannelId(channelEdge.getChannelId()); + Coins capacity = Coins.ofSatoshis(channelEdge.getCapacity()); + Pubkey node1Pubkey = Pubkey.create(channelEdge.getNode1Pub()); + Pubkey node2Pubkey = Pubkey.create(channelEdge.getNode2Pub()); + DirectedChannelEdge directedChannelEdge1 = new DirectedChannelEdge( + channelId, + capacity, + node1Pubkey, + node2Pubkey, + toPolicy(channelEdge.getNode1Policy()) + ); + DirectedChannelEdge directedChannelEdge2 = new DirectedChannelEdge( + channelId, + capacity, + node2Pubkey, + node1Pubkey, + toPolicy(channelEdge.getNode2Policy()) + ); + channelEdges.add(directedChannelEdge1); + channelEdges.add(directedChannelEdge2); + } + return Optional.of(channelEdges); + } + + private Policy toPolicy(RoutingPolicy routingPolicy) { + return new Policy( + routingPolicy.getFeeRateMilliMsat(), + Coins.ofMilliSatoshis(routingPolicy.getFeeBaseMsat()), + !routingPolicy.getDisabled() + ); + } +} 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 8c3e97be..07f672ea 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 @@ -12,6 +12,8 @@ import lnrpc.ChanInfoRequest; import lnrpc.Channel; import lnrpc.ChannelCloseSummary; import lnrpc.ChannelEdge; +import lnrpc.ChannelGraph; +import lnrpc.ChannelGraphRequest; import lnrpc.ClosedChannelsRequest; import lnrpc.ForwardingHistoryRequest; import lnrpc.ForwardingHistoryResponse; @@ -182,6 +184,14 @@ public class GrpcService extends GrpcBase { .build())); } + @Timed + public Optional describeGraph() { + ChannelGraphRequest request = ChannelGraphRequest.newBuilder() + .setIncludeUnannounced(true) + .build(); + return get(() -> lightningStub.describeGraph(request)); + } + private Optional> getTransactionsWithoutCache() { return get( () -> lightningStub.getTransactions(GetTransactionsRequest.getDefaultInstance()).getTransactionsList() diff --git a/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcGraphTest.java b/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcGraphTest.java new file mode 100644 index 00000000..6428e723 --- /dev/null +++ b/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcGraphTest.java @@ -0,0 +1,113 @@ +package de.cotto.lndmanagej.grpc; + +import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.DirectedChannelEdge; +import de.cotto.lndmanagej.model.Policy; +import lnrpc.ChannelEdge; +import lnrpc.ChannelGraph; +import lnrpc.RoutingPolicy; +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 java.util.Set; + +import static de.cotto.lndmanagej.model.ChannelFixtures.CAPACITY; +import static de.cotto.lndmanagej.model.ChannelFixtures.CAPACITY_2; +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.PubkeyFixtures.PUBKEY; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_4; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GrpcGraphTest { + @Mock + private GrpcService grpcService; + + @InjectMocks + private GrpcGraph grpcGraph; + + @Test + void empty() { + when(grpcService.describeGraph()).thenReturn(Optional.empty()); + assertThat(grpcGraph.getChannelEdges()).isEmpty(); + } + + @Test + void no_edge() { + ChannelGraph channelGraph = ChannelGraph.getDefaultInstance(); + when(grpcService.describeGraph()).thenReturn(Optional.of(channelGraph)); + assertThat(grpcGraph.getChannelEdges()).contains(Set.of()); + } + + @Test + void two_edges() { + ChannelEdge edge1 = ChannelEdge.newBuilder() + .setChannelId(CHANNEL_ID.getShortChannelId()) + .setCapacity(CAPACITY.satoshis()) + .setNode1Pub(PUBKEY.toString()) + .setNode2Pub(PUBKEY_2.toString()) + .setNode1Policy(policy(0, 0, true)) + .setNode2Policy(policy(1, 0, false)) + .build(); + DirectedChannelEdge expectedEdge1 = new DirectedChannelEdge( + CHANNEL_ID, + CAPACITY, + PUBKEY, + PUBKEY_2, + new Policy(0, Coins.NONE, false) + ); + DirectedChannelEdge expectedEdge2 = new DirectedChannelEdge( + CHANNEL_ID, + CAPACITY, + PUBKEY_2, + PUBKEY, + new Policy(1, Coins.NONE, true) + ); + ChannelEdge edge2 = ChannelEdge.newBuilder() + .setChannelId(CHANNEL_ID_2.getShortChannelId()) + .setCapacity(CAPACITY_2.satoshis()) + .setNode1Pub(PUBKEY_3.toString()) + .setNode2Pub(PUBKEY_4.toString()) + .setNode1Policy(policy(456, 0, false)) + .setNode2Policy(policy(123, 1, false)) + .build(); + DirectedChannelEdge expectedEdge3 = new DirectedChannelEdge( + CHANNEL_ID_2, + CAPACITY_2, + PUBKEY_3, + PUBKEY_4, + new Policy(456, Coins.NONE, true) + ); + DirectedChannelEdge expectedEdge4 = new DirectedChannelEdge( + CHANNEL_ID_2, + CAPACITY_2, + PUBKEY_4, + PUBKEY_3, + new Policy(123, Coins.ofMilliSatoshis(1), true) + ); + ChannelGraph channelGraph = ChannelGraph.newBuilder() + .addEdges(edge1) + .addEdges(edge2) + .build(); + when(grpcService.describeGraph()).thenReturn(Optional.of(channelGraph)); + assertThat(grpcGraph.getChannelEdges().orElseThrow()).containsExactlyInAnyOrder( + expectedEdge1, expectedEdge2, expectedEdge3, expectedEdge4 + ); + } + + private RoutingPolicy policy(int feeRate, int baseFee, boolean disabled) { + return RoutingPolicy.newBuilder() + .setFeeRateMilliMsat(feeRate) + .setFeeBaseMsat(baseFee) + .setDisabled(disabled) + .build(); + } +} \ No newline at end of file diff --git a/model/src/main/java/de/cotto/lndmanagej/model/DirectedChannelEdge.java b/model/src/main/java/de/cotto/lndmanagej/model/DirectedChannelEdge.java new file mode 100644 index 00000000..24ca248f --- /dev/null +++ b/model/src/main/java/de/cotto/lndmanagej/model/DirectedChannelEdge.java @@ -0,0 +1,4 @@ +package de.cotto.lndmanagej.model; + +public record DirectedChannelEdge(ChannelId channelId, Coins capacity, Pubkey source, Pubkey target, Policy policy) { +} diff --git a/model/src/test/java/de/cotto/lndmanagej/model/DirectedChannelEdgeTest.java b/model/src/test/java/de/cotto/lndmanagej/model/DirectedChannelEdgeTest.java new file mode 100644 index 00000000..8d22c220 --- /dev/null +++ b/model/src/test/java/de/cotto/lndmanagej/model/DirectedChannelEdgeTest.java @@ -0,0 +1,38 @@ +package de.cotto.lndmanagej.model; + +import org.junit.jupiter.api.Test; + +import static de.cotto.lndmanagej.model.ChannelFixtures.CAPACITY; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.DirectedChannelEdgeFixtures.CHANNEL_EDGE_WITH_POLICY; +import static de.cotto.lndmanagej.model.PolicyFixtures.POLICY_1; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; +import static org.assertj.core.api.Assertions.assertThat; + +class DirectedChannelEdgeTest { + @Test + void channelId() { + assertThat(CHANNEL_EDGE_WITH_POLICY.channelId()).isEqualTo(CHANNEL_ID); + } + + @Test + void capacity() { + assertThat(CHANNEL_EDGE_WITH_POLICY.capacity()).isEqualTo(CAPACITY); + } + + @Test + void source() { + assertThat(CHANNEL_EDGE_WITH_POLICY.source()).isEqualTo(PUBKEY); + } + + @Test + void target() { + assertThat(CHANNEL_EDGE_WITH_POLICY.target()).isEqualTo(PUBKEY_2); + } + + @Test + void policy() { + assertThat(CHANNEL_EDGE_WITH_POLICY.policy()).isEqualTo(POLICY_1); + } +} \ No newline at end of file diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/DirectedChannelEdgeFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/DirectedChannelEdgeFixtures.java new file mode 100644 index 00000000..476ff574 --- /dev/null +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/DirectedChannelEdgeFixtures.java @@ -0,0 +1,17 @@ +package de.cotto.lndmanagej.model; + +import static de.cotto.lndmanagej.model.ChannelFixtures.CAPACITY; +import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.PolicyFixtures.POLICY_1; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; + +public class DirectedChannelEdgeFixtures { + public static final DirectedChannelEdge CHANNEL_EDGE_WITH_POLICY = new DirectedChannelEdge( + CHANNEL_ID, + CAPACITY, + PUBKEY, + PUBKEY_2, + POLICY_1 + ); +}