get available local balance

This commit is contained in:
Carsten Otto
2021-11-12 17:35:05 +01:00
parent def458ebbb
commit a746dfd664
11 changed files with 204 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.service.BalanceService;
import de.cotto.lndmanagej.service.ChannelService;
import de.cotto.lndmanagej.service.FeeService;
import de.cotto.lndmanagej.service.NodeService;
@@ -51,6 +52,9 @@ class LegacyControllerIT {
@MockBean
private FeeService feeService;
@MockBean
private BalanceService balanceService;
@Test
void getAlias() throws Exception {
when(nodeService.getAlias(PUBKEY)).thenReturn(ALIAS);
@@ -136,4 +140,12 @@ class LegacyControllerIT {
mockMvc.perform(get(CHANNEL_BASE + "/outgoing-base-fee"))
.andExpect(content().string(String.valueOf(BASE_FEE.milliSatoshis())));
}
@Test
void getAvailableLocalBalance() throws Exception {
Coins availableBalance = Coins.ofSatoshis(999);
when(balanceService.getAvailableLocalBalance(CHANNEL_ID)).thenReturn(availableBalance);
mockMvc.perform(get(CHANNEL_BASE + "/available-local-balance"))
.andExpect(content().string(String.valueOf(availableBalance.satoshis())));
}
}

View File

@@ -4,6 +4,7 @@ import de.cotto.lndmanagej.model.Channel;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.LocalChannel;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.service.BalanceService;
import de.cotto.lndmanagej.service.ChannelService;
import de.cotto.lndmanagej.service.FeeService;
import de.cotto.lndmanagej.service.NodeService;
@@ -25,17 +26,20 @@ public class LegacyController {
private final ChannelService channelService;
private final OwnNodeService ownNodeService;
private final FeeService feeService;
private final BalanceService balanceService;
public LegacyController(
NodeService nodeService,
ChannelService channelService,
OwnNodeService ownNodeService,
FeeService feeService
FeeService feeService,
BalanceService balanceService
) {
this.nodeService = nodeService;
this.channelService = channelService;
this.ownNodeService = ownNodeService;
this.feeService = feeService;
this.balanceService = balanceService;
}
@GetMapping("/node/{pubkey}/alias")
@@ -115,6 +119,11 @@ public class LegacyController {
return feeService.getOutgoingBaseFee(channelId).milliSatoshis();
}
@GetMapping("/channel/{channelId}/available-local-balance")
public long getAvailableLocalBalance(ChannelId channelId) {
return balanceService.getAvailableLocalBalance(channelId).satoshis();
}
private Stream<ChannelId> getOpenChannelIdsSorted() {
return channelService.getOpenChannels().stream()
.map(Channel::getId)

View File

@@ -0,0 +1,34 @@
package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.grpc.GrpcChannels;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.model.LocalChannel;
import org.springframework.stereotype.Component;
@Component
public class BalanceService {
private final GrpcChannels grpcChannels;
public BalanceService(GrpcChannels grpcChannels) {
this.grpcChannels = grpcChannels;
}
public Coins getLocalBalance(ChannelId channelId) {
return grpcChannels.getChannel(channelId).map(LocalChannel::getLocalBalance).orElse(Coins.NONE);
}
public Coins getLocalReserve(ChannelId channelId) {
return grpcChannels.getChannel(channelId).map(LocalChannel::getLocalReserve).orElse(Coins.NONE);
}
public Coins getAvailableLocalBalance(ChannelId channelId) {
Coins available = grpcChannels.getChannel(channelId)
.map(c -> c.getLocalBalance().subtract(c.getLocalReserve()))
.orElse(Coins.NONE);
if (available.isNegative()) {
return Coins.NONE;
}
return available;
}
}

View File

@@ -1,8 +1,10 @@
package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.model.Channel;
import de.cotto.lndmanagej.model.ChannelFixtures;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.model.LocalChannel;
import de.cotto.lndmanagej.service.BalanceService;
import de.cotto.lndmanagej.service.ChannelService;
import de.cotto.lndmanagej.service.FeeService;
import de.cotto.lndmanagej.service.NodeService;
@@ -53,6 +55,9 @@ class LegacyControllerTest {
@Mock
private FeeService feeService;
@Mock
private BalanceService balanceService;
@Test
void getAlias() {
when(nodeService.getAlias(PUBKEY)).thenReturn(ALIAS);
@@ -131,14 +136,16 @@ class LegacyControllerTest {
@Test
void getPeerPubkeys() {
LocalChannel channel2 = new LocalChannel(ChannelFixtures.create(PUBKEY, PUBKEY_3, CHANNEL_ID_2), PUBKEY);
Channel channel = ChannelFixtures.create(PUBKEY, PUBKEY_3, CHANNEL_ID_2);
LocalChannel channel2 = new LocalChannel(channel, PUBKEY, Coins.NONE, Coins.NONE);
when(channelService.getOpenChannels()).thenReturn(Set.of(LOCAL_CHANNEL, channel2));
assertThat(legacyController.getPeerPubkeys()).isEqualTo(PUBKEY_2 + "\n" + PUBKEY_3);
}
@Test
void getPeerPubkeys_sorted() {
LocalChannel channel2 = new LocalChannel(ChannelFixtures.create(PUBKEY, PUBKEY_3, CHANNEL_ID_2), PUBKEY);
Channel channel = ChannelFixtures.create(PUBKEY, PUBKEY_3, CHANNEL_ID_2);
LocalChannel channel2 = new LocalChannel(channel, PUBKEY, Coins.NONE, Coins.NONE);
when(channelService.getOpenChannels()).thenReturn(Set.of(channel2, LOCAL_CHANNEL));
assertThat(legacyController.getPeerPubkeys()).isEqualTo(PUBKEY_2 + "\n" + PUBKEY_3);
}
@@ -172,4 +179,10 @@ class LegacyControllerTest {
when(feeService.getOutgoingBaseFee(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(10L));
assertThat(legacyController.getOutgoingBaseFee(CHANNEL_ID)).isEqualTo(10);
}
@Test
void getAvailableLocalBalance() {
when(balanceService.getAvailableLocalBalance(CHANNEL_ID)).thenReturn(Coins.ofSatoshis(123L));
assertThat(legacyController.getAvailableLocalBalance(CHANNEL_ID)).isEqualTo(123);
}
}

View File

@@ -0,0 +1,62 @@
package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.grpc.GrpcChannels;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.model.LocalChannel;
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.ChannelFixtures.CHANNEL;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_BALANCE;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_CHANNEL;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.RESERVE_LOCAL;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class BalanceServiceTest {
@InjectMocks
private BalanceService balanceService;
@Mock
private GrpcChannels grpcChannels;
@Test
void getLocalBalance() {
when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_CHANNEL));
assertThat(balanceService.getLocalBalance(CHANNEL_ID)).isEqualTo(LOCAL_BALANCE);
}
@Test
void getLocalBalance_no_channel() {
when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.empty());
assertThat(balanceService.getLocalBalance(CHANNEL_ID)).isEqualTo(Coins.NONE);
}
@Test
void getLocalReserve() {
when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_CHANNEL));
assertThat(balanceService.getLocalReserve(CHANNEL_ID)).isEqualTo(RESERVE_LOCAL);
}
@Test
void getAvailableLocalBalance() {
when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_CHANNEL));
assertThat(balanceService.getAvailableLocalBalance(CHANNEL_ID))
.isEqualTo(LOCAL_BALANCE.subtract(RESERVE_LOCAL));
}
@Test
void getAvailableLocalBalance_negative() {
LocalChannel localChannel = new LocalChannel(CHANNEL, PUBKEY, Coins.ofSatoshis(99), Coins.ofSatoshis(100));
when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.of(localChannel));
assertThat(balanceService.getAvailableLocalBalance(CHANNEL_ID)).isEqualTo(Coins.NONE);
}
}

View File

@@ -1,7 +1,9 @@
package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.grpc.GrpcChannels;
import de.cotto.lndmanagej.model.Channel;
import de.cotto.lndmanagej.model.ChannelFixtures;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.model.LocalChannel;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -37,7 +39,8 @@ class ChannelServiceTest {
@Test
void getOpenChannelsWith_ignores_channel_to_other_node() {
LocalChannel localChannel2 = new LocalChannel(ChannelFixtures.create(PUBKEY, PUBKEY_3, CHANNEL_ID_2), PUBKEY);
Channel channel = ChannelFixtures.create(PUBKEY, PUBKEY_3, CHANNEL_ID_2);
LocalChannel localChannel2 = new LocalChannel(channel, PUBKEY, Coins.NONE, Coins.NONE);
when(grpcChannels.getChannels()).thenReturn(Set.of(LOCAL_CHANNEL, localChannel2, LOCAL_CHANNEL_3));
assertThat(channelService.getOpenChannelsWith(PUBKEY_2))
.containsExactlyInAnyOrder(LOCAL_CHANNEL, LOCAL_CHANNEL_3);

View File

@@ -7,6 +7,7 @@ import de.cotto.lndmanagej.model.LocalChannel;
import de.cotto.lndmanagej.model.Pubkey;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.util.Set;
import static java.util.stream.Collectors.toSet;
@@ -31,6 +32,15 @@ public class GrpcChannels {
.collect(toSet());
}
public Optional<LocalChannel> getChannel(ChannelId channelId) {
Pubkey ownPubkey = grpcGetInfo.getPubkey();
long expectedChannelId = channelId.shortChannelId();
return grpcService.getChannels().stream()
.filter(c -> c.getChanId() == expectedChannelId)
.map(lndChannel -> toChannel(lndChannel, ownPubkey))
.findFirst();
}
private LocalChannel toChannel(lnrpc.Channel lndChannel, Pubkey ownPubkey) {
Channel channel = Channel.builder()
.withChannelId(ChannelId.fromShortChannelId(lndChannel.getChanId()))
@@ -38,7 +48,9 @@ public class GrpcChannels {
.withNode1(ownPubkey)
.withNode2(Pubkey.create(lndChannel.getRemotePubkey()))
.build();
return new LocalChannel(channel, ownPubkey);
Coins localBalance = Coins.ofSatoshis(lndChannel.getLocalBalance());
Coins localReserve = Coins.ofSatoshis(lndChannel.getLocalConstraints().getChanReserveSat());
return new LocalChannel(channel, ownPubkey, localBalance, localReserve);
}
}

View File

@@ -1,5 +1,6 @@
package de.cotto.lndmanagej.grpc;
import de.cotto.lndmanagej.model.ChannelId;
import lnrpc.Channel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -12,6 +13,7 @@ import java.util.List;
import static de.cotto.lndmanagej.model.ChannelFixtures.CAPACITY;
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.LocalChannelFixtures.LOCAL_CHANNEL;
import static de.cotto.lndmanagej.model.NodeFixtures.NODE_2;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
@@ -35,19 +37,30 @@ class GrpcChannelsTest {
}
@Test
void no_channels() {
void getChannels_no_channels() {
assertThat(grpcChannels.getChannels()).isEmpty();
}
@Test
void one_channel() {
when(grpcService.getChannels()).thenReturn(List.of(channel()));
void getChannels_one_channel() {
when(grpcService.getChannels()).thenReturn(List.of(channel(CHANNEL_ID)));
assertThat(grpcChannels.getChannels()).containsExactly(LOCAL_CHANNEL);
}
private Channel channel() {
@Test
void getChannel() {
when(grpcService.getChannels()).thenReturn(List.of(channel(CHANNEL_ID_2), channel(CHANNEL_ID)));
assertThat(grpcChannels.getChannel(CHANNEL_ID)).contains(LOCAL_CHANNEL);
}
@Test
void getChannel_empty() {
assertThat(grpcChannels.getChannel(CHANNEL_ID)).isEmpty();
}
private Channel channel(ChannelId channelId) {
return Channel.newBuilder()
.setChanId(CHANNEL_ID.shortChannelId())
.setChanId(channelId.shortChannelId())
.setCapacity(CAPACITY.satoshis())
.setRemotePubkey(NODE_2.pubkey().toString())
.build();

View File

@@ -4,9 +4,13 @@ import java.util.Set;
public class LocalChannel extends Channel {
private final Pubkey remotePubkey;
private final Coins localBalance;
private final Coins localReserve;
public LocalChannel(Channel channel, Pubkey ownPubkey) {
public LocalChannel(Channel channel, Pubkey ownPubkey, Coins localBalance, Coins localReserve) {
super(channel.getId(), channel.getCapacity(), channel.getPubkeys());
this.localBalance = localBalance;
this.localReserve = localReserve;
Set<Pubkey> pubkeys = channel.getPubkeys();
remotePubkey = pubkeys.stream()
.filter(pubkey -> !ownPubkey.equals(pubkey))
@@ -20,4 +24,12 @@ public class LocalChannel extends Channel {
public Pubkey getRemotePubkey() {
return remotePubkey;
}
public Coins getLocalBalance() {
return localBalance;
}
public Coins getLocalReserve() {
return localReserve;
}
}

View File

@@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test;
import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL_2;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_BALANCE;
import static de.cotto.lndmanagej.model.LocalChannelFixtures.LOCAL_CHANNEL;
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;
@@ -13,20 +15,27 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
class LocalChannelTest {
@Test
void getRemotePubkey() {
LocalChannel localChannel = new LocalChannel(ChannelFixtures.create(PUBKEY_2, PUBKEY, CHANNEL_ID), PUBKEY);
Channel channel = ChannelFixtures.create(PUBKEY_2, PUBKEY, CHANNEL_ID);
LocalChannel localChannel = new LocalChannel(channel, PUBKEY, Coins.NONE, Coins.NONE);
assertThat(localChannel.getRemotePubkey()).isEqualTo(PUBKEY_2);
}
@Test
void getRemotePubkey_swapped() {
LocalChannel localChannel = new LocalChannel(ChannelFixtures.create(PUBKEY_3, PUBKEY_2, CHANNEL_ID), PUBKEY_3);
Channel channel = ChannelFixtures.create(PUBKEY_3, PUBKEY_2, CHANNEL_ID);
LocalChannel localChannel = new LocalChannel(channel, PUBKEY_3, Coins.NONE, Coins.NONE);
assertThat(localChannel.getRemotePubkey()).isEqualTo(PUBKEY_2);
}
@Test
void ownPubkey_not_in_pubkey_set() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new LocalChannel(CHANNEL_2, PUBKEY_3))
.isThrownBy(() -> new LocalChannel(CHANNEL_2, PUBKEY_3, Coins.NONE, Coins.NONE))
.withMessage("Channel must have given pubkey as peer");
}
@Test
void getLocalBalance() {
assertThat(LOCAL_CHANNEL.getLocalBalance()).isEqualTo(LOCAL_BALANCE);
}
}

View File

@@ -7,8 +7,15 @@ import static de.cotto.lndmanagej.model.ChannelFixtures.CHANNEL_TO_NODE_3;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
public class LocalChannelFixtures {
public static final LocalChannel LOCAL_CHANNEL = new LocalChannel(CHANNEL, PUBKEY);
public static final LocalChannel LOCAL_CHANNEL_2 = new LocalChannel(CHANNEL_2, PUBKEY);
public static final LocalChannel LOCAL_CHANNEL_3 = new LocalChannel(CHANNEL_3, PUBKEY);
public static final LocalChannel LOCAL_CHANNEL_TO_NODE_3 = new LocalChannel(CHANNEL_TO_NODE_3, PUBKEY);
public static final Coins LOCAL_BALANCE = Coins.ofSatoshis(1_000);
public static final Coins RESERVE_LOCAL = Coins.ofSatoshis(100);
public static final LocalChannel LOCAL_CHANNEL =
new LocalChannel(CHANNEL, PUBKEY, LOCAL_BALANCE, RESERVE_LOCAL);
public static final LocalChannel LOCAL_CHANNEL_2 =
new LocalChannel(CHANNEL_2, PUBKEY, LOCAL_BALANCE, RESERVE_LOCAL);
public static final LocalChannel LOCAL_CHANNEL_3 =
new LocalChannel(CHANNEL_3, PUBKEY, LOCAL_BALANCE, RESERVE_LOCAL);
public static final LocalChannel LOCAL_CHANNEL_TO_NODE_3 =
new LocalChannel(CHANNEL_TO_NODE_3, PUBKEY, LOCAL_BALANCE, RESERVE_LOCAL);
}