add incoming-fee-rate and outgoing-fee-rate endpoints

This commit is contained in:
Carsten Otto
2021-11-12 14:47:26 +01:00
parent b8f9af1d72
commit 6cbced801a
16 changed files with 420 additions and 7 deletions

View File

@@ -1,6 +1,7 @@
package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.service.ChannelService;
import de.cotto.lndmanagej.service.FeeService;
import de.cotto.lndmanagej.service.NodeService;
import de.cotto.lndmanagej.service.OwnNodeService;
import org.junit.jupiter.api.Test;
@@ -39,6 +40,9 @@ class LegacyControllerIT {
@MockBean
private OwnNodeService ownNodeService;
@MockBean
private FeeService feeService;
@Test
void getAlias() throws Exception {
when(nodeService.getAlias(PUBKEY)).thenReturn(ALIAS);
@@ -75,4 +79,18 @@ class LegacyControllerIT {
mockMvc.perform(get("/legacy/peer-pubkeys"))
.andExpect(content().string(PUBKEY_2 + "\n" + PUBKEY_3));
}
@Test
void getIncomingFeeRate() throws Exception {
when(feeService.getIncomingFeeRate(CHANNEL_ID)).thenReturn(123L);
mockMvc.perform(get("/legacy/channel/" + CHANNEL_ID + "/incoming-fee-rate"))
.andExpect(content().string("123"));
}
@Test
void getOutgoingFeeRate() throws Exception {
when(feeService.getOutgoingFeeRate(CHANNEL_ID)).thenReturn(123L);
mockMvc.perform(get("/legacy/channel/" + CHANNEL_ID + "/outgoing-fee-rate"))
.andExpect(content().string("123"));
}
}

View File

@@ -0,0 +1,24 @@
package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.model.ChannelId;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import javax.annotation.Nonnull;
@Component
public class ChannelIdConverter implements Converter<String, ChannelId> {
public ChannelIdConverter() {
// default constructor
}
@Override
public ChannelId convert(@Nonnull String source) {
try {
long shortChannelId = Long.parseLong(source);
return ChannelId.fromShortChannelId(shortChannelId);
} catch (NumberFormatException numberFormatException) {
return ChannelId.fromCompactForm(source);
}
}
}

View File

@@ -5,6 +5,7 @@ import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.LocalChannel;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.service.ChannelService;
import de.cotto.lndmanagej.service.FeeService;
import de.cotto.lndmanagej.service.NodeService;
import de.cotto.lndmanagej.service.OwnNodeService;
import org.springframework.web.bind.annotation.GetMapping;
@@ -21,11 +22,18 @@ public class LegacyController {
private final NodeService nodeService;
private final ChannelService channelService;
private final OwnNodeService ownNodeService;
private final FeeService feeService;
public LegacyController(NodeService nodeService, ChannelService channelService, OwnNodeService ownNodeService) {
public LegacyController(
NodeService nodeService,
ChannelService channelService,
OwnNodeService ownNodeService,
FeeService feeService
) {
this.nodeService = nodeService;
this.channelService = channelService;
this.ownNodeService = ownNodeService;
this.feeService = feeService;
}
@GetMapping("/node/{pubkey}/alias")
@@ -56,4 +64,14 @@ public class LegacyController {
public boolean syncedToChain() {
return ownNodeService.isSyncedToChain();
}
@GetMapping("/channel/{channelId}/incoming-fee-rate")
public long getIncomingFeeRate(@PathVariable ChannelId channelId) {
return feeService.getIncomingFeeRate(channelId);
}
@GetMapping("/channel/{channelId}/outgoing-fee-rate")
public long getOutgoingFeeRate(@PathVariable ChannelId channelId) {
return feeService.getOutgoingFeeRate(channelId);
}
}

View File

@@ -0,0 +1,22 @@
package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.grpc.GrpcFees;
import de.cotto.lndmanagej.model.ChannelId;
import org.springframework.stereotype.Component;
@Component
public class FeeService {
private final GrpcFees grpcFees;
public FeeService(GrpcFees grpcFees) {
this.grpcFees = grpcFees;
}
public long getIncomingFeeRate(ChannelId channelId) {
return grpcFees.getIncomingFeeRate(channelId).orElseThrow(IllegalStateException::new);
}
public long getOutgoingFeeRate(ChannelId channelId) {
return grpcFees.getOutgoingFeeRate(channelId).orElseThrow(IllegalStateException::new);
}
}

View File

@@ -0,0 +1,23 @@
package de.cotto.lndmanagej.controller;
import org.junit.jupiter.api.Test;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static org.assertj.core.api.Assertions.assertThat;
class ChannelIdConverterTest {
@Test
void convert() {
assertThat(new ChannelIdConverter().convert(CHANNEL_ID.toString())).isEqualTo(CHANNEL_ID);
}
@Test
void convert_from_compact_form_with_x() {
assertThat(new ChannelIdConverter().convert("712345x123x1")).isEqualTo(CHANNEL_ID);
}
@Test
void convert_from_compact_form() {
assertThat(new ChannelIdConverter().convert("712345:123:1")).isEqualTo(CHANNEL_ID);
}
}

View File

@@ -3,6 +3,7 @@ package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.model.ChannelFixtures;
import de.cotto.lndmanagej.model.LocalChannel;
import de.cotto.lndmanagej.service.ChannelService;
import de.cotto.lndmanagej.service.FeeService;
import de.cotto.lndmanagej.service.NodeService;
import de.cotto.lndmanagej.service.OwnNodeService;
import org.junit.jupiter.api.Test;
@@ -40,6 +41,9 @@ class LegacyControllerTest {
@Mock
private OwnNodeService ownNodeService;
@Mock
private FeeService feeService;
@Test
void getAlias() {
when(nodeService.getAlias(PUBKEY)).thenReturn(ALIAS);
@@ -93,4 +97,16 @@ class LegacyControllerTest {
when(channelService.getOpenChannels()).thenReturn(Set.of(LOCAL_CHANNEL, LOCAL_CHANNEL_2));
assertThat(legacyController.getPeerPubkeys()).isEqualTo(PUBKEY_2.toString());
}
@Test
void getIncomingFeeRate() {
when(feeService.getIncomingFeeRate(CHANNEL_ID)).thenReturn(123L);
assertThat(legacyController.getIncomingFeeRate(CHANNEL_ID)).isEqualTo(123);
}
@Test
void getOutgoingFeeRate() {
when(feeService.getOutgoingFeeRate(CHANNEL_ID)).thenReturn(123L);
assertThat(legacyController.getOutgoingFeeRate(CHANNEL_ID)).isEqualTo(123);
}
}

View File

@@ -0,0 +1,48 @@
package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.grpc.GrpcFees;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class FeeServiceTest {
@InjectMocks
private FeeService feeService;
@Mock
private GrpcFees grpcFees;
@Test
void getIncomingFeeRate() {
when(grpcFees.getIncomingFeeRate(CHANNEL_ID)).thenReturn(Optional.of(123L));
assertThat(feeService.getIncomingFeeRate(CHANNEL_ID)).isEqualTo(123);
}
@Test
void getIncomingFeeRate_empty() {
when(grpcFees.getIncomingFeeRate(CHANNEL_ID)).thenReturn(Optional.empty());
assertThatIllegalStateException().isThrownBy(() -> feeService.getIncomingFeeRate(CHANNEL_ID));
}
@Test
void getOutgoingFeeRate() {
when(grpcFees.getOutgoingFeeRate(CHANNEL_ID)).thenReturn(Optional.of(123L));
assertThat(feeService.getOutgoingFeeRate(CHANNEL_ID)).isEqualTo(123);
}
@Test
void getOutgoingFeeRate_empty() {
when(grpcFees.getOutgoingFeeRate(CHANNEL_ID)).thenReturn(Optional.empty());
assertThatIllegalStateException().isThrownBy(() -> feeService.getOutgoingFeeRate(CHANNEL_ID));
}
}

View File

@@ -0,0 +1,49 @@
package de.cotto.lndmanagej.grpc;
import de.cotto.lndmanagej.model.ChannelId;
import lnrpc.RoutingPolicy;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class GrpcChannelPolicy {
private final GrpcService grpcService;
private final GrpcGetInfo grpcGetInfo;
public GrpcChannelPolicy(GrpcService grpcService, GrpcGetInfo grpcGetInfo) {
this.grpcService = grpcService;
this.grpcGetInfo = grpcGetInfo;
}
public Optional<RoutingPolicy> getLocalPolicy(ChannelId channelId) {
String ownPubkey = grpcGetInfo.getPubkey().toString();
return grpcService.getChannelEdge(channelId).map(
channelEdge -> {
if (ownPubkey.equals(channelEdge.getNode1Pub())) {
return channelEdge.getNode1Policy();
} else if (ownPubkey.equals(channelEdge.getNode2Pub())) {
return channelEdge.getNode2Policy();
} else {
return null;
}
}
);
}
public Optional<RoutingPolicy> getRemotePolicy(ChannelId channelId) {
String ownPubkey = grpcGetInfo.getPubkey().toString();
return grpcService.getChannelEdge(channelId).map(
channelEdge -> {
if (ownPubkey.equals(channelEdge.getNode2Pub())) {
return channelEdge.getNode1Policy();
} else if (ownPubkey.equals(channelEdge.getNode1Pub())) {
return channelEdge.getNode2Policy();
} else {
return null;
}
}
);
}
}

View File

@@ -25,7 +25,7 @@ public class GrpcChannels {
}
public Set<LocalChannel> getChannels() {
Pubkey ownPubkey = grpcGetInfo.getPubkey().orElseThrow();
Pubkey ownPubkey = grpcGetInfo.getPubkey();
return grpcService.getChannels().stream()
.map(lndChannel -> toChannel(lndChannel, ownPubkey))
.collect(toSet());

View File

@@ -0,0 +1,24 @@
package de.cotto.lndmanagej.grpc;
import de.cotto.lndmanagej.model.ChannelId;
import lnrpc.RoutingPolicy;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class GrpcFees {
private final GrpcChannelPolicy grpcChannelPolicy;
public GrpcFees(GrpcChannelPolicy grpcChannelPolicy) {
this.grpcChannelPolicy = grpcChannelPolicy;
}
public Optional<Long> getOutgoingFeeRate(ChannelId channelId) {
return grpcChannelPolicy.getLocalPolicy(channelId).map(RoutingPolicy::getFeeRateMilliMsat);
}
public Optional<Long> getIncomingFeeRate(ChannelId channelId) {
return grpcChannelPolicy.getRemotePolicy(channelId).map(RoutingPolicy::getFeeRateMilliMsat);
}
}

View File

@@ -4,19 +4,27 @@ import de.cotto.lndmanagej.model.Pubkey;
import lnrpc.GetInfoResponse;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
@Component
public class GrpcGetInfo {
private final GrpcService grpcService;
@Nullable
private Pubkey pubkey;
public GrpcGetInfo(GrpcService grpcService) {
this.grpcService = grpcService;
}
public Optional<Pubkey> getPubkey() {
return grpcService.getInfo().map(GetInfoResponse::getIdentityPubkey).map(Pubkey::create);
public Pubkey getPubkey() {
if (pubkey == null) {
pubkey = grpcService.getInfo().map(GetInfoResponse::getIdentityPubkey).map(Pubkey::create).orElseThrow();
}
return Objects.requireNonNull(pubkey);
}
public Optional<String> getAlias() {

View File

@@ -1,9 +1,12 @@
package de.cotto.lndmanagej.grpc;
import de.cotto.lndmanagej.LndConfiguration;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Pubkey;
import io.grpc.StatusRuntimeException;
import lnrpc.ChanInfoRequest;
import lnrpc.Channel;
import lnrpc.ChannelEdge;
import lnrpc.GetInfoResponse;
import lnrpc.LightningGrpc;
import lnrpc.ListChannelsRequest;
@@ -74,4 +77,9 @@ public class GrpcService {
public Optional<NodeInfo> getNodeInfo(Pubkey pubkey) {
return get(() -> lightningStub.getNodeInfo(NodeInfoRequest.newBuilder().setPubKey(pubkey.toString()).build()));
}
public Optional<ChannelEdge> getChannelEdge(ChannelId channelId) {
ChanInfoRequest build = ChanInfoRequest.newBuilder().setChanId(channelId.shortChannelId()).build();
return get(() -> lightningStub.getChanInfo(build));
}
}

View File

@@ -0,0 +1,99 @@
package de.cotto.lndmanagej.grpc;
import de.cotto.lndmanagej.model.Pubkey;
import lnrpc.ChannelEdge;
import lnrpc.RoutingPolicy;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
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 org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class GrpcChannelPolicyTest {
private static final int FEE_RATE_FIRST = 123;
private static final int FEE_RATE_SECOND = 456;
@InjectMocks
private GrpcChannelPolicy grpcChannelPolicy;
@Mock
private GrpcService grpcService;
@Mock
private GrpcGetInfo grpcGetInfo;
@BeforeEach
void setUp() {
when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY);
}
@Test
void getLocalPolicy_local_first() {
when(grpcService.getChannelEdge(CHANNEL_ID)).thenReturn(Optional.of(channelEdge(PUBKEY, PUBKEY_2)));
assertThat(grpcChannelPolicy.getLocalPolicy(CHANNEL_ID)).contains(routingPolicy(FEE_RATE_FIRST));
}
@Test
void getLocalPolicy_local_second() {
when(grpcService.getChannelEdge(CHANNEL_ID)).thenReturn(Optional.of(channelEdge(PUBKEY_2, PUBKEY)));
assertThat(grpcChannelPolicy.getLocalPolicy(CHANNEL_ID)).contains(routingPolicy(FEE_RATE_SECOND));
}
@Test
void getLocalPolicy_not_local() {
when(grpcService.getChannelEdge(CHANNEL_ID)).thenReturn(Optional.of(channelEdge(PUBKEY_2, PUBKEY_3)));
assertThat(grpcChannelPolicy.getLocalPolicy(CHANNEL_ID)).isEmpty();
}
@Test
void getLocalPolicy_empty() {
assertThat(grpcChannelPolicy.getLocalPolicy(CHANNEL_ID)).isEmpty();
}
@Test
void getRemotePolicy_local_first() {
when(grpcService.getChannelEdge(CHANNEL_ID)).thenReturn(Optional.of(channelEdge(PUBKEY, PUBKEY_2)));
assertThat(grpcChannelPolicy.getRemotePolicy(CHANNEL_ID)).contains(routingPolicy(FEE_RATE_SECOND));
}
@Test
void getRemotePolicy_local_second() {
when(grpcService.getChannelEdge(CHANNEL_ID)).thenReturn(Optional.of(channelEdge(PUBKEY_2, PUBKEY)));
assertThat(grpcChannelPolicy.getRemotePolicy(CHANNEL_ID)).contains(routingPolicy(FEE_RATE_FIRST));
}
@Test
void getRemotePolicy_not_local() {
when(grpcService.getChannelEdge(CHANNEL_ID)).thenReturn(Optional.of(channelEdge(PUBKEY_2, PUBKEY_3)));
assertThat(grpcChannelPolicy.getRemotePolicy(CHANNEL_ID)).isEmpty();
}
@Test
void getRemotePolicy_empty() {
assertThat(grpcChannelPolicy.getRemotePolicy(CHANNEL_ID)).isEmpty();
}
private ChannelEdge channelEdge(Pubkey firstPubkey, Pubkey secondPubkey) {
return ChannelEdge.newBuilder()
.setNode1Pub(firstPubkey.toString())
.setNode2Pub(secondPubkey.toString())
.setNode1Policy(routingPolicy(FEE_RATE_FIRST))
.setNode2Policy(routingPolicy(FEE_RATE_SECOND))
.build();
}
private RoutingPolicy routingPolicy(int feeRate) {
return RoutingPolicy.newBuilder().setFeeRateMilliMsat(feeRate).build();
}
}

View File

@@ -9,7 +9,6 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Optional;
import static de.cotto.lndmanagej.model.ChannelFixtures.CAPACITY;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
@@ -32,7 +31,7 @@ class GrpcChannelsTest {
@BeforeEach
void setUp() {
when(grpcGetInfo.getPubkey()).thenReturn(Optional.of(PUBKEY));
when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY);
}
@Test

View File

@@ -0,0 +1,50 @@
package de.cotto.lndmanagej.grpc;
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 static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class GrpcFeesTest {
@InjectMocks
private GrpcFees grpcFees;
@Mock
private GrpcChannelPolicy grpcChannelPolicy;
@Test
void getOutgoingFeeRate() {
when(grpcChannelPolicy.getLocalPolicy(CHANNEL_ID)).thenReturn(Optional.of(routingPolicy()));
assertThat(grpcFees.getOutgoingFeeRate(CHANNEL_ID)).contains(123L);
}
@Test
void getOutgoingFeeRate_empty() {
assertThat(grpcFees.getOutgoingFeeRate(CHANNEL_ID)).isEmpty();
}
@Test
void getIncomingFeeRate() {
when(grpcChannelPolicy.getRemotePolicy(CHANNEL_ID)).thenReturn(Optional.of(routingPolicy()));
assertThat(grpcFees.getIncomingFeeRate(CHANNEL_ID)).contains(123L);
}
@Test
void getIncomingFeeRate_empty() {
assertThat(grpcFees.getIncomingFeeRate(CHANNEL_ID)).isEmpty();
}
private RoutingPolicy routingPolicy() {
return RoutingPolicy.newBuilder().setFeeRateMilliMsat(123).build();
}
}

View File

@@ -55,7 +55,14 @@ class GrpcGetInfoTest {
@Test
void getPubkey() {
assertThat(grpcGetInfo.getPubkey()).contains(PUBKEY);
assertThat(grpcGetInfo.getPubkey()).isEqualTo(PUBKEY);
}
@Test
void getPubkey_cached() {
grpcGetInfo.getPubkey();
when(grpcService.getInfo()).thenReturn(Optional.empty());
assertThat(grpcGetInfo.getPubkey()).isEqualTo(PUBKEY);
}
@Test