mirror of
https://github.com/aljazceru/lnd-manageJ.git
synced 2026-01-29 18:54:29 +01:00
@@ -83,4 +83,9 @@ public class BalanceService {
|
||||
public Optional<Coins> getLocalBalanceMaximum(ChannelId channelId, int days) {
|
||||
return balancesDao.getLocalBalanceMaximum(channelId, days);
|
||||
}
|
||||
|
||||
@Timed
|
||||
public Optional<Coins> getLocalBalanceAverage(ChannelId channelId, int days) {
|
||||
return balancesDao.getLocalBalanceAverage(channelId, days);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.cotto.lndmanagej.service;
|
||||
import de.cotto.lndmanagej.configuration.ConfigurationService;
|
||||
import de.cotto.lndmanagej.model.Channel;
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.FeeReport;
|
||||
import de.cotto.lndmanagej.model.LocalOpenChannel;
|
||||
import de.cotto.lndmanagej.model.Pubkey;
|
||||
@@ -29,6 +30,7 @@ public class RatingService {
|
||||
private final RebalanceService rebalanceService;
|
||||
private final PolicyService policyService;
|
||||
private final ConfigurationService configurationService;
|
||||
private final BalanceService balanceService;
|
||||
|
||||
public RatingService(
|
||||
ChannelService channelService,
|
||||
@@ -36,7 +38,8 @@ public class RatingService {
|
||||
FeeService feeService,
|
||||
RebalanceService rebalanceService,
|
||||
PolicyService policyService,
|
||||
ConfigurationService configurationService
|
||||
ConfigurationService configurationService,
|
||||
BalanceService balanceService
|
||||
) {
|
||||
this.channelService = channelService;
|
||||
this.ownNodeService = ownNodeService;
|
||||
@@ -44,6 +47,7 @@ public class RatingService {
|
||||
this.rebalanceService = rebalanceService;
|
||||
this.policyService = policyService;
|
||||
this.configurationService = configurationService;
|
||||
this.balanceService = balanceService;
|
||||
}
|
||||
|
||||
public Rating getRatingForPeer(Pubkey peer) {
|
||||
@@ -70,13 +74,15 @@ public class RatingService {
|
||||
long feeRate = policyService.getMinimumFeeRateTo(localOpenChannel.getRemotePubkey()).orElse(0L);
|
||||
long localAvailableMilliSat = localOpenChannel.getBalanceInformation().localAvailable().milliSatoshis();
|
||||
double millionSat = 1.0 * localAvailableMilliSat / 1_000 / 1_000_000;
|
||||
long averageSat = balanceService.getLocalBalanceAverage(channelId, (int) durationForAnalysis.toDays())
|
||||
.orElse(Coins.NONE).satoshis();
|
||||
|
||||
long rating = feeReport.earned().milliSatoshis();
|
||||
rating += feeReport.sourced().milliSatoshis();
|
||||
rating += rebalanceReport.supportAsSourceAmount().milliSatoshis() / 10_000;
|
||||
rating += rebalanceReport.supportAsTargetAmount().milliSatoshis() / 10_000;
|
||||
rating += (long) (1.0 * feeRate * millionSat / 10);
|
||||
double scaledByLiquidity = rating / Math.max(1, millionSat);
|
||||
double scaledByLiquidity = 1.0 * rating * 1_000_000 / averageSat;
|
||||
double scaledByDays = scaledByLiquidity / durationForAnalysis.toDays();
|
||||
return Optional.of(new Rating((long) scaledByDays));
|
||||
}
|
||||
|
||||
@@ -134,6 +134,19 @@ class BalanceServiceTest {
|
||||
assertThat(balanceService.getLocalBalanceMaximum(CHANNEL_ID, days)).contains(coins);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceAverage_empty() {
|
||||
assertThat(balanceService.getLocalBalanceAverage(CHANNEL_ID, 14)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceAverage() {
|
||||
int days = 14;
|
||||
Coins coins = Coins.ofSatoshis(456);
|
||||
when(balancesDao.getLocalBalanceAverage(CHANNEL_ID, days)).thenReturn(Optional.of(coins));
|
||||
assertThat(balanceService.getLocalBalanceAverage(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));
|
||||
|
||||
@@ -35,8 +35,10 @@ import static de.cotto.lndmanagej.model.OpenInitiator.LOCAL;
|
||||
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;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assumptions.assumeThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -68,6 +70,9 @@ class RatingServiceTest {
|
||||
@Mock
|
||||
private ConfigurationService configurationService;
|
||||
|
||||
@Mock
|
||||
private BalanceService balanceService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
int daysAhead = LOCAL_OPEN_CHANNEL_2.getId().getBlockHeight() + 100 * 24 * 60 / 10;
|
||||
@@ -75,6 +80,8 @@ class RatingServiceTest {
|
||||
lenient().when(feeService.getFeeReportForChannel(any(), any())).thenReturn(FeeReport.EMPTY);
|
||||
lenient().when(rebalanceService.getReportForChannel(any(), any())).thenReturn(RebalanceReport.EMPTY);
|
||||
lenient().when(configurationService.getIntegerValue(any())).thenReturn(Optional.empty());
|
||||
lenient().when(balanceService.getLocalBalanceAverage(any(), anyInt()))
|
||||
.thenReturn(Optional.of(Coins.ofSatoshis(1_000_000)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -208,27 +215,30 @@ class RatingServiceTest {
|
||||
long maxEarnings = (long) (1.0 * feeRate * balanceMilliSat / 1_000 / 1_000_000.0);
|
||||
assumeThat(maxEarnings).isGreaterThanOrEqualTo(10 * ANALYSIS_DAYS);
|
||||
assertThat(ratingService.getRatingForChannel(CHANNEL_ID))
|
||||
.contains(new Rating((maxEarnings / 10 / ANALYSIS_DAYS) / 2));
|
||||
.contains(new Rating(maxEarnings / 10 / ANALYSIS_DAYS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void divided_by_million_sats_local() {
|
||||
Coins localAvailable = Coins.ofSatoshis(2_500_000);
|
||||
void divided_by_average_million_sats_local() {
|
||||
Coins localAvailableAverage = Coins.ofSatoshis(2_500_000);
|
||||
long expected = (long) ((1 + 100_000) / 2.5);
|
||||
assertScaledRating(localAvailable, expected);
|
||||
assertScaledRating(localAvailableAverage, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void not_divided_if_zero_balance() {
|
||||
Coins localAvailable = Coins.NONE;
|
||||
long expected = 100_000;
|
||||
assertScaledRating(localAvailable, expected);
|
||||
void zero_balance_on_average() {
|
||||
when(balanceService.getLocalBalanceAverage(CHANNEL_ID, ANALYSIS_DAYS))
|
||||
.thenReturn(Optional.of(Coins.NONE));
|
||||
when(channelService.getOpenChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL));
|
||||
when(feeService.getFeeReportForChannel(CHANNEL_ID, DEFAULT_DURATION_FOR_ANALYSIS))
|
||||
.thenReturn(new FeeReport(Coins.ofMilliSatoshis(100_000 * ANALYSIS_DAYS), Coins.NONE));
|
||||
assertThatCode(() -> ratingService.getRatingForChannel(CHANNEL_ID)).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void not_divided_if_local_balance_below_one_million() {
|
||||
Coins localAvailable = Coins.ofSatoshis(999_999);
|
||||
long expected = 100_000;
|
||||
void divided_by_average_million_sats_local_if_less_than_one_million_sat() {
|
||||
Coins localAvailable = Coins.ofSatoshis(500_000);
|
||||
long expected = 100_000 * 2;
|
||||
assertScaledRating(localAvailable, expected);
|
||||
}
|
||||
|
||||
@@ -247,6 +257,8 @@ class RatingServiceTest {
|
||||
private void assertScaledRating(Coins localAvailable, long expectedRating) {
|
||||
LocalOpenChannel localOpenChannel = getLocalOpenChannel(localAvailable);
|
||||
when(channelService.getOpenChannel(CHANNEL_ID)).thenReturn(Optional.of(localOpenChannel));
|
||||
when(balanceService.getLocalBalanceAverage(CHANNEL_ID, ANALYSIS_DAYS))
|
||||
.thenReturn(Optional.of(localAvailable));
|
||||
when(feeService.getFeeReportForChannel(CHANNEL_ID, DEFAULT_DURATION_FOR_ANALYSIS))
|
||||
.thenReturn(new FeeReport(Coins.ofMilliSatoshis(100_000 * ANALYSIS_DAYS), Coins.NONE));
|
||||
assertThat(ratingService.getRatingForChannel(CHANNEL_ID)).contains(new Rating(expectedRating));
|
||||
|
||||
@@ -147,7 +147,38 @@ class BalancesRepositoryIT {
|
||||
.contains(balancesLessThanMaximum.balanceInformation().localBalance());
|
||||
}
|
||||
|
||||
@Test
|
||||
void average_empty() {
|
||||
assertThat(repository.getAverageLocalBalance(
|
||||
CHANNEL_ID.getShortChannelId(),
|
||||
14
|
||||
)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void average() {
|
||||
assumeThat(localBalance(BALANCES_OLD) > localBalance(BALANCES)).isTrue();
|
||||
repository.save(BalancesJpaDto.fromModel(BALANCES));
|
||||
repository.save(BalancesJpaDto.fromModel(BALANCES_OLD));
|
||||
long sum = BALANCES.balanceInformation().localBalance().satoshis() +
|
||||
BALANCES_OLD.balanceInformation().localBalance().satoshis();
|
||||
assertThat(repository.getAverageLocalBalance(CHANNEL_ID.getShortChannelId(), 14)).contains(sum / 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void average_too_old() {
|
||||
LocalDateTime now = TIMESTAMP;
|
||||
Balances balanceYoungEnough = createBalances(Coins.ofSatoshis(123_456), now);
|
||||
Balances balancesMaximumButOld = createBalances(Coins.ofSatoshis(1), now.minusDays(1));
|
||||
long timestamp = balancesMaximumButOld.timestamp().toEpochSecond(ZoneOffset.UTC);
|
||||
|
||||
repository.save(BalancesJpaDto.fromModel(balanceYoungEnough));
|
||||
repository.save(BalancesJpaDto.fromModel(balancesMaximumButOld));
|
||||
assertThat(repository.getAverageLocalBalance(CHANNEL_ID.getShortChannelId(), timestamp))
|
||||
.contains(balanceYoungEnough.balanceInformation().localBalance().satoshis());
|
||||
}
|
||||
|
||||
private long localBalance(Balances balances) {
|
||||
return balances.balanceInformation().localBalance().milliSatoshis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ public interface BalancesDao {
|
||||
Optional<Coins> getLocalBalanceMinimum(ChannelId channelId, int days);
|
||||
|
||||
Optional<Coins> getLocalBalanceMaximum(ChannelId channelId, int days);
|
||||
|
||||
Optional<Coins> getLocalBalanceAverage(ChannelId channelId, int days);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,15 @@ class BalancesDaoImpl implements BalancesDao {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Coins> getLocalBalanceAverage(ChannelId channelId, int days) {
|
||||
long timestamp = getTimestamp(days);
|
||||
return balancesRepository.getAverageLocalBalance(
|
||||
channelId.getShortChannelId(),
|
||||
timestamp
|
||||
).map(Coins::ofSatoshis);
|
||||
}
|
||||
|
||||
private Optional<Coins> getLocalBalance(Optional<BalancesJpaDto> balances) {
|
||||
return balances
|
||||
.map(BalancesJpaDto::toModel)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.cotto.lndmanagej.balances.persistence;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -10,4 +11,7 @@ public interface BalancesRepository extends JpaRepository<BalancesJpaDto, String
|
||||
Optional<BalancesJpaDto> findTopByChannelIdAndTimestampAfterOrderByLocalBalance(long channelId, long timestamp);
|
||||
|
||||
Optional<BalancesJpaDto> findTopByChannelIdAndTimestampAfterOrderByLocalBalanceDesc(long channelId, long timestamp);
|
||||
|
||||
@Query("SELECT avg(localBalance) FROM BalancesJpaDto b WHERE b.channelId = ?1 AND b.timestamp > ?2")
|
||||
Optional<Long> getAverageLocalBalance(long channelId, long timestamp);
|
||||
}
|
||||
|
||||
@@ -115,9 +115,28 @@ class BalancesDaoImplTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceAverage_empty() {
|
||||
assertThat(dao.getLocalBalanceAverage(CHANNEL_ID, 1)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocalBalanceAverage() {
|
||||
int days = 14;
|
||||
Coins averageLocalBalance = Coins.ofSatoshis(456);
|
||||
when(balancesRepository.getAverageLocalBalance(eq(CHANNEL_ID.getShortChannelId()), anyLong()))
|
||||
.thenReturn(Optional.of(averageLocalBalance.satoshis()));
|
||||
assertThat(dao.getLocalBalanceAverage(CHANNEL_ID, days)).contains(averageLocalBalance);
|
||||
|
||||
long expectedTimestamp = ZonedDateTime.now(ZoneOffset.UTC).minusDays(days).toEpochSecond();
|
||||
verify(balancesRepository).getAverageLocalBalance(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user