get LN graph via gRPC

This commit is contained in:
Carsten Otto
2022-03-13 15:24:43 +01:00
parent b3df494383
commit cf39bfd62b
6 changed files with 258 additions and 0 deletions

View File

@@ -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<Object, Optional<Set<DirectedChannelEdge>>> 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<Set<DirectedChannelEdge>> getChannelEdges() {
return channelEdgeCache.get("");
}
public Optional<Set<DirectedChannelEdge>> getChannelEdgesWithoutCache() {
ChannelGraph channelGraph = grpcService.describeGraph().orElse(null);
if (channelGraph == null) {
return Optional.empty();
}
Set<DirectedChannelEdge> 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()
);
}
}

View File

@@ -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<ChannelGraph> describeGraph() {
ChannelGraphRequest request = ChannelGraphRequest.newBuilder()
.setIncludeUnannounced(true)
.build();
return get(() -> lightningStub.describeGraph(request));
}
private Optional<List<Transaction>> getTransactionsWithoutCache() {
return get(
() -> lightningStub.getTransactions(GetTransactionsRequest.getDefaultInstance()).getTransactionsList()

View File

@@ -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();
}
}

View File

@@ -0,0 +1,4 @@
package de.cotto.lndmanagej.model;
public record DirectedChannelEdge(ChannelId channelId, Coins capacity, Pubkey source, Pubkey target, Policy policy) {
}

View File

@@ -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);
}
}

View File

@@ -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
);
}