mirror of
https://github.com/aljazceru/lnd-manageJ.git
synced 2026-01-30 03:04:38 +01:00
add channel balance fluctuation warning
This commit is contained in:
@@ -7,6 +7,7 @@ dependencies {
|
||||
implementation project(':forwarding-history')
|
||||
implementation project(':grpc-adapter')
|
||||
implementation project(':model')
|
||||
implementation project(':balances')
|
||||
implementation project(':onlinepeers')
|
||||
implementation project(':selfpayments')
|
||||
implementation project(':transactions')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.cotto.lndmanagej.service;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import de.cotto.lndmanagej.balances.BalancesDao;
|
||||
import de.cotto.lndmanagej.grpc.GrpcChannels;
|
||||
import de.cotto.lndmanagej.model.BalanceInformation;
|
||||
import de.cotto.lndmanagej.model.Channel;
|
||||
@@ -16,10 +17,16 @@ import java.util.Optional;
|
||||
public class BalanceService {
|
||||
private final GrpcChannels grpcChannels;
|
||||
private final ChannelService channelService;
|
||||
private final BalancesDao balancesDao;
|
||||
|
||||
public BalanceService(GrpcChannels grpcChannels, ChannelService channelService) {
|
||||
public BalanceService(
|
||||
GrpcChannels grpcChannels,
|
||||
ChannelService channelService,
|
||||
BalancesDao balancesDao
|
||||
) {
|
||||
this.grpcChannels = grpcChannels;
|
||||
this.channelService = channelService;
|
||||
this.balancesDao = balancesDao;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -66,4 +73,14 @@ public class BalanceService {
|
||||
return grpcChannels.getChannel(channelId)
|
||||
.map(LocalOpenChannel::getBalanceInformation);
|
||||
}
|
||||
|
||||
@Timed
|
||||
public Optional<Coins> getLocalBalanceMinimum(ChannelId channelId, int days) {
|
||||
return balancesDao.getLocalBalanceMinimum(channelId, days);
|
||||
}
|
||||
|
||||
@Timed
|
||||
public Optional<Coins> getLocalBalanceMaximum(ChannelId channelId, int days) {
|
||||
return balancesDao.getLocalBalanceMaximum(channelId, days);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.cotto.lndmanagej.service;
|
||||
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.LocalChannel;
|
||||
import de.cotto.lndmanagej.model.warnings.ChannelBalanceFluctuationWarning;
|
||||
import de.cotto.lndmanagej.model.warnings.ChannelWarning;
|
||||
import de.cotto.lndmanagej.service.warnings.ChannelWarningsProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Component
|
||||
public class ChannelBalanceFluctuationWarningsProvider implements ChannelWarningsProvider {
|
||||
private static final int LOWER_THRESHOLD = 10;
|
||||
private static final int UPPER_THRESHOLD = 90;
|
||||
private static final int DAYS = 14;
|
||||
|
||||
private final ChannelService channelService;
|
||||
private final BalanceService balanceService;
|
||||
|
||||
public ChannelBalanceFluctuationWarningsProvider(ChannelService channelService, BalanceService balanceService) {
|
||||
this.channelService = channelService;
|
||||
this.balanceService = balanceService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ChannelWarning> getChannelWarnings(ChannelId channelId) {
|
||||
return Stream.of(getBalanceFluctuatingWarning(channelId)).flatMap(Optional::stream);
|
||||
}
|
||||
|
||||
private Optional<ChannelWarning> getBalanceFluctuatingWarning(ChannelId channelId) {
|
||||
Coins capacity = channelService.getLocalChannel(channelId).map(LocalChannel::getCapacity).orElse(null);
|
||||
if (capacity == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Coins min = balanceService.getLocalBalanceMinimum(channelId, DAYS).orElse(null);
|
||||
Coins max = balanceService.getLocalBalanceMaximum(channelId, DAYS).orElse(null);
|
||||
if (min == null || max == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
int minPercentage = (int) (min.milliSatoshis() * 100.0 / capacity.milliSatoshis());
|
||||
int maxPercentage = (int) (max.milliSatoshis() * 100.0 / capacity.milliSatoshis());
|
||||
if (minPercentage < LOWER_THRESHOLD && maxPercentage > UPPER_THRESHOLD) {
|
||||
return Optional.of(new ChannelBalanceFluctuationWarning(minPercentage, maxPercentage, DAYS));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.cotto.lndmanagej.service;
|
||||
|
||||
import de.cotto.lndmanagej.balances.BalancesDao;
|
||||
import de.cotto.lndmanagej.grpc.GrpcChannels;
|
||||
import de.cotto.lndmanagej.model.BalanceInformation;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
@@ -33,6 +34,9 @@ class BalanceServiceTest {
|
||||
@Mock
|
||||
private ChannelService channelService;
|
||||
|
||||
@Mock
|
||||
private BalancesDao balancesDao;
|
||||
|
||||
@Test
|
||||
void getBalanceInformation_for_pubkey() {
|
||||
BalanceInformation expected = new BalanceInformation(
|
||||
@@ -104,6 +108,32 @@ class BalanceServiceTest {
|
||||
assertThat(balanceService.getAvailableRemoteBalanceForPeer(PUBKEY)).isEqualTo(Coins.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceMinimum_empty() {
|
||||
assertThat(balanceService.getLocalBalanceMinimum(CHANNEL_ID, 7)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceMinimum() {
|
||||
int days = 7;
|
||||
Coins coins = Coins.ofSatoshis(123);
|
||||
when(balancesDao.getLocalBalanceMinimum(CHANNEL_ID, days)).thenReturn(Optional.of(coins));
|
||||
assertThat(balanceService.getLocalBalanceMinimum(CHANNEL_ID, days)).contains(coins);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceMaximum_empty() {
|
||||
assertThat(balanceService.getLocalBalanceMaximum(CHANNEL_ID, 7)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceMaximum() {
|
||||
int days = 7;
|
||||
Coins coins = Coins.ofSatoshis(123);
|
||||
when(balancesDao.getLocalBalanceMaximum(CHANNEL_ID, days)).thenReturn(Optional.of(coins));
|
||||
assertThat(balanceService.getLocalBalanceMaximum(CHANNEL_ID, days)).contains(coins);
|
||||
}
|
||||
|
||||
private void mockChannels() {
|
||||
when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL));
|
||||
when(grpcChannels.getChannel(CHANNEL_ID_2)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL_2));
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package de.cotto.lndmanagej.service;
|
||||
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.warnings.ChannelBalanceFluctuationWarning;
|
||||
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 javax.annotation.Nullable;
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
|
||||
import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ChannelBalanceFluctuationWarningsProviderTest {
|
||||
private static final int LOWER_THRESHOLD = 10;
|
||||
private static final int UPPER_THRESHOLD = 90;
|
||||
private static final int DAYS = 14;
|
||||
|
||||
@InjectMocks
|
||||
private ChannelBalanceFluctuationWarningsProvider warningsProvider;
|
||||
|
||||
@Mock
|
||||
private ChannelService channelService;
|
||||
|
||||
@Mock
|
||||
private BalanceService balanceService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelWarnings_open_channel_not_found() {
|
||||
when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.empty());
|
||||
assertThat(warningsProvider.getChannelWarnings(CHANNEL_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelWarnings_both_within_bounds() {
|
||||
mockMinMax(LOWER_THRESHOLD, UPPER_THRESHOLD);
|
||||
assertThat(warningsProvider.getChannelWarnings(CHANNEL_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelWarnings_minimum_not_found() {
|
||||
mockMinMax(null, UPPER_THRESHOLD);
|
||||
assertThat(warningsProvider.getChannelWarnings(CHANNEL_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelWarnings_maximum_not_found() {
|
||||
mockMinMax(LOWER_THRESHOLD, null);
|
||||
assertThat(warningsProvider.getChannelWarnings(CHANNEL_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelWarnings_minimum_and_maximum_not_found() {
|
||||
mockMinMax(null, null);
|
||||
assertThat(warningsProvider.getChannelWarnings(CHANNEL_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelWarnings_just_below_minimum() {
|
||||
mockMinMax(LOWER_THRESHOLD - 1, UPPER_THRESHOLD);
|
||||
assertThat(warningsProvider.getChannelWarnings(CHANNEL_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelWarnings_just_above_maximum() {
|
||||
mockMinMax(LOWER_THRESHOLD, UPPER_THRESHOLD + 1);
|
||||
assertThat(warningsProvider.getChannelWarnings(CHANNEL_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelWarnings() {
|
||||
mockMinMax(LOWER_THRESHOLD - 1, UPPER_THRESHOLD + 1);
|
||||
ChannelBalanceFluctuationWarning expectedWarning =
|
||||
new ChannelBalanceFluctuationWarning(LOWER_THRESHOLD - 1, UPPER_THRESHOLD + 1, DAYS);
|
||||
assertThat(warningsProvider.getChannelWarnings(CHANNEL_ID)).contains(expectedWarning);
|
||||
}
|
||||
|
||||
private void mockMinMax(@Nullable Integer min, @Nullable Integer max) {
|
||||
Coins capacity = LOCAL_OPEN_CHANNEL.getCapacity();
|
||||
if (min == null) {
|
||||
when(balanceService.getLocalBalanceMinimum(CHANNEL_ID, DAYS))
|
||||
.thenReturn(Optional.empty());
|
||||
} else {
|
||||
Coins minLocalAvailable = Coins.ofSatoshis((long) (capacity.satoshis() / 100.0 * min));
|
||||
when(balanceService.getLocalBalanceMinimum(CHANNEL_ID, DAYS))
|
||||
.thenReturn(Optional.of(minLocalAvailable));
|
||||
}
|
||||
if (max == null) {
|
||||
when(balanceService.getLocalBalanceMaximum(CHANNEL_ID, DAYS))
|
||||
.thenReturn(Optional.empty());
|
||||
} else {
|
||||
Coins maxLocalAvailable = Coins.ofSatoshis((long) (capacity.satoshis() / 100.0 * max));
|
||||
when(balanceService.getLocalBalanceMaximum(CHANNEL_ID, DAYS))
|
||||
.thenReturn(Optional.of(maxLocalAvailable));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,26 @@
|
||||
package de.cotto.lndmanagej.balances.persistence;
|
||||
|
||||
import de.cotto.lndmanagej.balances.Balances;
|
||||
import de.cotto.lndmanagej.model.BalanceInformation;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import static de.cotto.lndmanagej.balances.BalancesFixtures.BALANCES;
|
||||
import static de.cotto.lndmanagej.balances.BalancesFixtures.BALANCES_OLD;
|
||||
import static de.cotto.lndmanagej.balances.BalancesFixtures.TIMESTAMP;
|
||||
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.LOCAL_BALANCE;
|
||||
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.LOCAL_RESERVE;
|
||||
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.REMOTE_BALANCE;
|
||||
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.REMOTE_RESERVE;
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_2;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assumptions.assumeThat;
|
||||
|
||||
@DataJpaTest
|
||||
class BalancesRepositoryIT {
|
||||
@@ -48,4 +60,94 @@ class BalancesRepositoryIT {
|
||||
assertThat(repository.findTopByChannelIdOrderByTimestampDesc(CHANNEL_ID.getShortChannelId()))
|
||||
.map(BalancesJpaDto::toModel).contains(BALANCES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimum_empty() {
|
||||
assertThat(repository.findTopByChannelIdAndTimestampAfterOrderByLocalBalance(
|
||||
CHANNEL_ID.getShortChannelId(),
|
||||
14
|
||||
)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimum() {
|
||||
assumeThat(localBalance(BALANCES) < localBalance(BALANCES_OLD)).isTrue();
|
||||
repository.save(BalancesJpaDto.fromModel(BALANCES));
|
||||
repository.save(BalancesJpaDto.fromModel(BALANCES_OLD));
|
||||
assertThat(repository.findTopByChannelIdAndTimestampAfterOrderByLocalBalance(
|
||||
CHANNEL_ID.getShortChannelId(),
|
||||
14
|
||||
)).map(BalancesJpaDto::toModel)
|
||||
.map(Balances::balanceInformation)
|
||||
.map(BalanceInformation::localBalance)
|
||||
.contains(BALANCES.balanceInformation().localBalance());
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimum_too_old() {
|
||||
LocalDateTime now = TIMESTAMP;
|
||||
Balances balancesMoreThanMinimum = createBalances(LOCAL_BALANCE.add(Coins.ofSatoshis(1)), now);
|
||||
Balances balancesMinimumButOld = createBalances(LOCAL_BALANCE, now.minusDays(1));
|
||||
long timestamp = balancesMinimumButOld.timestamp().toEpochSecond(ZoneOffset.UTC);
|
||||
|
||||
repository.save(BalancesJpaDto.fromModel(balancesMinimumButOld));
|
||||
repository.save(BalancesJpaDto.fromModel(balancesMoreThanMinimum));
|
||||
assertThat(repository.findTopByChannelIdAndTimestampAfterOrderByLocalBalance(
|
||||
CHANNEL_ID.getShortChannelId(),
|
||||
timestamp
|
||||
)).map(BalancesJpaDto::toModel)
|
||||
.map(Balances::balanceInformation)
|
||||
.map(BalanceInformation::localBalance)
|
||||
.contains(balancesMoreThanMinimum.balanceInformation().localBalance());
|
||||
}
|
||||
|
||||
private Balances createBalances(Coins localBalance, LocalDateTime timestamp) {
|
||||
BalanceInformation balanceInformation =
|
||||
new BalanceInformation(localBalance, LOCAL_RESERVE, REMOTE_BALANCE, REMOTE_RESERVE);
|
||||
return new Balances(timestamp, CHANNEL_ID, balanceInformation);
|
||||
}
|
||||
|
||||
@Test
|
||||
void maximum_empty() {
|
||||
assertThat(repository.findTopByChannelIdAndTimestampAfterOrderByLocalBalanceDesc(
|
||||
CHANNEL_ID.getShortChannelId(),
|
||||
14
|
||||
)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void maximum() {
|
||||
assumeThat(localBalance(BALANCES_OLD) > localBalance(BALANCES)).isTrue();
|
||||
repository.save(BalancesJpaDto.fromModel(BALANCES));
|
||||
repository.save(BalancesJpaDto.fromModel(BALANCES_OLD));
|
||||
assertThat(repository.findTopByChannelIdAndTimestampAfterOrderByLocalBalanceDesc(
|
||||
CHANNEL_ID.getShortChannelId(),
|
||||
14
|
||||
)).map(BalancesJpaDto::toModel)
|
||||
.map(Balances::balanceInformation)
|
||||
.map(BalanceInformation::localBalance)
|
||||
.contains(BALANCES_OLD.balanceInformation().localBalance());
|
||||
}
|
||||
|
||||
@Test
|
||||
void maximum_too_old() {
|
||||
LocalDateTime now = TIMESTAMP;
|
||||
Balances balancesLessThanMaximum = createBalances(LOCAL_BALANCE.subtract(Coins.ofSatoshis(1)), now);
|
||||
Balances balancesMaximumButOld = createBalances(LOCAL_BALANCE, now.minusDays(1));
|
||||
long timestamp = balancesMaximumButOld.timestamp().toEpochSecond(ZoneOffset.UTC);
|
||||
|
||||
repository.save(BalancesJpaDto.fromModel(balancesMaximumButOld));
|
||||
repository.save(BalancesJpaDto.fromModel(balancesLessThanMaximum));
|
||||
assertThat(repository.findTopByChannelIdAndTimestampAfterOrderByLocalBalance(
|
||||
CHANNEL_ID.getShortChannelId(),
|
||||
timestamp
|
||||
)).map(BalancesJpaDto::toModel)
|
||||
.map(Balances::balanceInformation)
|
||||
.map(BalanceInformation::localBalance)
|
||||
.contains(balancesLessThanMaximum.balanceInformation().localBalance());
|
||||
}
|
||||
|
||||
private long localBalance(Balances balances) {
|
||||
return balances.balanceInformation().localBalance().milliSatoshis();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.cotto.lndmanagej.balances;
|
||||
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -8,4 +9,8 @@ public interface BalancesDao {
|
||||
void saveBalances(Balances balances);
|
||||
|
||||
Optional<Balances> getMostRecentBalances(ChannelId channelId);
|
||||
|
||||
Optional<Coins> getLocalBalanceMinimum(ChannelId channelId, int days);
|
||||
|
||||
Optional<Coins> getLocalBalanceMaximum(ChannelId channelId, int days);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ package de.cotto.lndmanagej.balances.persistence;
|
||||
|
||||
import de.cotto.lndmanagej.balances.Balances;
|
||||
import de.cotto.lndmanagej.balances.BalancesDao;
|
||||
import de.cotto.lndmanagej.model.BalanceInformation;
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
@@ -27,4 +31,36 @@ class BalancesDaoImpl implements BalancesDao {
|
||||
return balancesRepository.findTopByChannelIdOrderByTimestampDesc(channelId.getShortChannelId())
|
||||
.map(BalancesJpaDto::toModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Coins> getLocalBalanceMinimum(ChannelId channelId, int days) {
|
||||
return getLocalBalance(
|
||||
balancesRepository.findTopByChannelIdAndTimestampAfterOrderByLocalBalance(
|
||||
channelId.getShortChannelId(),
|
||||
getTimestamp(days)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Coins> getLocalBalanceMaximum(ChannelId channelId, int days) {
|
||||
long timestamp = getTimestamp(days);
|
||||
return getLocalBalance(
|
||||
balancesRepository.findTopByChannelIdAndTimestampAfterOrderByLocalBalanceDesc(
|
||||
channelId.getShortChannelId(),
|
||||
timestamp
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private Optional<Coins> getLocalBalance(Optional<BalancesJpaDto> balances) {
|
||||
return balances
|
||||
.map(BalancesJpaDto::toModel)
|
||||
.map(Balances::balanceInformation)
|
||||
.map(BalanceInformation::localBalance);
|
||||
}
|
||||
|
||||
private long getTimestamp(int daysInPast) {
|
||||
return ZonedDateTime.now(ZoneOffset.UTC).minusDays(daysInPast).toEpochSecond();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,8 @@ import java.util.Optional;
|
||||
|
||||
public interface BalancesRepository extends JpaRepository<BalancesJpaDto, String> {
|
||||
Optional<BalancesJpaDto> findTopByChannelIdOrderByTimestampDesc(long channelId);
|
||||
|
||||
Optional<BalancesJpaDto> findTopByChannelIdAndTimestampAfterOrderByLocalBalance(long channelId, long timestamp);
|
||||
|
||||
Optional<BalancesJpaDto> findTopByChannelIdAndTimestampAfterOrderByLocalBalanceDesc(long channelId, long timestamp);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.cotto.lndmanagej.balances.persistence;
|
||||
|
||||
import de.cotto.lndmanagej.balances.Balances;
|
||||
import de.cotto.lndmanagej.model.BalanceInformation;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -7,14 +10,21 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.cotto.lndmanagej.balances.BalancesFixtures.BALANCES;
|
||||
import static de.cotto.lndmanagej.balances.BalancesFixtures.TIMESTAMP;
|
||||
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.BALANCE_INFORMATION;
|
||||
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.LOCAL_RESERVE;
|
||||
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.REMOTE_BALANCE;
|
||||
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.REMOTE_RESERVE;
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.longThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -60,4 +70,54 @@ class BalancesDaoImplTest {
|
||||
.thenReturn(Optional.of(BalancesJpaDto.fromModel(BALANCES)));
|
||||
assertThat(dao.getMostRecentBalances(CHANNEL_ID)).contains(BALANCES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceMinimum_empty() {
|
||||
assertThat(dao.getLocalBalanceMinimum(CHANNEL_ID, 7)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceMinimum() {
|
||||
int days = 14;
|
||||
Coins localBalance = Coins.ofSatoshis(456);
|
||||
Balances balances = getWithLocalBalance(localBalance);
|
||||
when(balancesRepository.findTopByChannelIdAndTimestampAfterOrderByLocalBalance(
|
||||
eq(CHANNEL_ID.getShortChannelId()),
|
||||
anyLong()
|
||||
)).thenReturn(Optional.of(BalancesJpaDto.fromModel(balances)));
|
||||
assertThat(dao.getLocalBalanceMinimum(CHANNEL_ID, days)).contains(localBalance);
|
||||
|
||||
long expectedTimestamp = ZonedDateTime.now(ZoneOffset.UTC).minusDays(days).toEpochSecond();
|
||||
verify(balancesRepository).findTopByChannelIdAndTimestampAfterOrderByLocalBalance(anyLong(),
|
||||
longThat(timestamp -> timestamp > 0.95 * expectedTimestamp && timestamp < 1.05 * expectedTimestamp)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceMaximum_empty() {
|
||||
assertThat(dao.getLocalBalanceMaximum(CHANNEL_ID, 7)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceMaximum() {
|
||||
int days = 14;
|
||||
Coins localBalance = Coins.ofSatoshis(456);
|
||||
Balances balances = getWithLocalBalance(localBalance);
|
||||
when(balancesRepository.findTopByChannelIdAndTimestampAfterOrderByLocalBalanceDesc(
|
||||
eq(CHANNEL_ID.getShortChannelId()),
|
||||
anyLong()
|
||||
)).thenReturn(Optional.of(BalancesJpaDto.fromModel(balances)));
|
||||
assertThat(dao.getLocalBalanceMaximum(CHANNEL_ID, days)).contains(localBalance);
|
||||
|
||||
long expectedTimestamp = ZonedDateTime.now(ZoneOffset.UTC).minusDays(days).toEpochSecond();
|
||||
verify(balancesRepository).findTopByChannelIdAndTimestampAfterOrderByLocalBalanceDesc(anyLong(),
|
||||
longThat(timestamp -> timestamp > 0.95 * expectedTimestamp && timestamp < 1.05 * expectedTimestamp)
|
||||
);
|
||||
}
|
||||
|
||||
private Balances getWithLocalBalance(Coins localBalance) {
|
||||
BalanceInformation balanceInformation =
|
||||
new BalanceInformation(localBalance, LOCAL_RESERVE, REMOTE_BALANCE, REMOTE_RESERVE);
|
||||
return new Balances(TIMESTAMP, CHANNEL_ID, balanceInformation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.cotto.lndmanagej.model.warnings;
|
||||
|
||||
public record ChannelBalanceFluctuationWarning(
|
||||
int minLocalBalancePercentage,
|
||||
int maxLocalBalancePercentage,
|
||||
int days
|
||||
) implements ChannelWarning {
|
||||
@Override
|
||||
public String description() {
|
||||
return "Channel balance fluctuated between %d%% and %d%% in the past %d days".formatted(
|
||||
minLocalBalancePercentage,
|
||||
maxLocalBalancePercentage,
|
||||
days
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.cotto.lndmanagej.model.warnings;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static de.cotto.lndmanagej.model.warnings.ChannelWarningFixtures.CHANNEL_BALANCE_FLUCTUATION_WARNING;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ChannelBalanceFluctuationWarningTest {
|
||||
@Test
|
||||
void minLocalBalancePercentage() {
|
||||
assertThat(CHANNEL_BALANCE_FLUCTUATION_WARNING.minLocalBalancePercentage()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxLocalBalancePercentage() {
|
||||
assertThat(CHANNEL_BALANCE_FLUCTUATION_WARNING.maxLocalBalancePercentage()).isEqualTo(97);
|
||||
}
|
||||
|
||||
@Test
|
||||
void days() {
|
||||
assertThat(CHANNEL_BALANCE_FLUCTUATION_WARNING.days()).isEqualTo(7);
|
||||
}
|
||||
|
||||
@Test
|
||||
void description() {
|
||||
assertThat(CHANNEL_BALANCE_FLUCTUATION_WARNING.description())
|
||||
.isEqualTo("Channel balance fluctuated between 2% and 97% in the past 7 days");
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,6 @@ package de.cotto.lndmanagej.model.warnings;
|
||||
|
||||
public class ChannelWarningFixtures {
|
||||
public static final ChannelNumUpdatesWarning CHANNEL_NUM_UPDATES_WARNING = new ChannelNumUpdatesWarning(101_000L);
|
||||
public static final ChannelBalanceFluctuationWarning CHANNEL_BALANCE_FLUCTUATION_WARNING =
|
||||
new ChannelBalanceFluctuationWarning(2, 97, 7);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user