mirror of
https://github.com/aljazceru/lnd-manageJ.git
synced 2026-01-20 06:24:28 +01:00
keep information about liquidity bounds
and use it for pickhardt payments
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package de.cotto.lndmanagej.service;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import de.cotto.lndmanagej.caching.CacheBuilder;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.LiquidityBounds;
|
||||
import de.cotto.lndmanagej.model.Pubkey;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
public class LiquidityBoundsService {
|
||||
private final MissionControlService missionControlService;
|
||||
private final LoadingCache<TwoPubkeys, LiquidityBounds> entries;
|
||||
|
||||
public LiquidityBoundsService(MissionControlService missionControlService) {
|
||||
this.missionControlService = missionControlService;
|
||||
entries = new CacheBuilder()
|
||||
.withSoftValues(true)
|
||||
.build(ignored -> new LiquidityBounds());
|
||||
}
|
||||
|
||||
@Timed
|
||||
public Optional<Coins> getAssumedLiquidityUpperBound(Pubkey source, Pubkey target) {
|
||||
Coins fromMissionControl = missionControlService.getMinimumOfRecentFailures(source, target).orElse(null);
|
||||
Coins fromPayments = getInfo(source, target).getUpperBound().orElse(null);
|
||||
if (fromMissionControl == null && fromPayments == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (fromMissionControl == null) {
|
||||
return Optional.of(Objects.requireNonNull(fromPayments));
|
||||
}
|
||||
Coins oneSatBelowFailure = fromMissionControl.subtract(Coins.ofSatoshis(1));
|
||||
if (fromPayments == null) {
|
||||
return Optional.of(oneSatBelowFailure);
|
||||
}
|
||||
return Optional.of(oneSatBelowFailure.minimum(fromPayments));
|
||||
}
|
||||
|
||||
@Timed
|
||||
public Coins getAssumedLiquidityLowerBound(Pubkey source, Pubkey target) {
|
||||
return getInfo(source, target).getLowerBound();
|
||||
}
|
||||
|
||||
public void markAsMoved(Pubkey source, Pubkey target, Coins amount) {
|
||||
getInfo(source, target).move(amount);
|
||||
}
|
||||
|
||||
public void markAsAvailable(Pubkey source, Pubkey target, Coins amount) {
|
||||
getInfo(source, target).available(amount);
|
||||
}
|
||||
|
||||
public void markAsUnavailable(Pubkey source, Pubkey target, Coins amount) {
|
||||
getInfo(source, target).unavailable(amount);
|
||||
}
|
||||
|
||||
private LiquidityBounds getInfo(Pubkey source, Pubkey target) {
|
||||
return entries.get(new TwoPubkeys(source, target));
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedVariable")
|
||||
private record TwoPubkeys(Pubkey source, Pubkey target) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package de.cotto.lndmanagej.service;
|
||||
|
||||
import de.cotto.lndmanagej.grpc.GrpcChannelPolicy;
|
||||
import de.cotto.lndmanagej.grpc.GrpcGetInfo;
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.FailureCode;
|
||||
import de.cotto.lndmanagej.model.HexString;
|
||||
import de.cotto.lndmanagej.model.PaymentAttemptHop;
|
||||
import de.cotto.lndmanagej.model.PaymentListener;
|
||||
import de.cotto.lndmanagej.model.Pubkey;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.cotto.lndmanagej.model.FailureCode.TEMPORARY_CHANNEL_FAILURE;
|
||||
|
||||
@Service
|
||||
public class LiquidityInformationUpdater implements PaymentListener {
|
||||
private final GrpcGetInfo grpcGetInfo;
|
||||
private final GrpcChannelPolicy grpcChannelPolicy;
|
||||
private final LiquidityBoundsService liquidityBoundsService;
|
||||
|
||||
public LiquidityInformationUpdater(
|
||||
GrpcGetInfo grpcGetInfo,
|
||||
GrpcChannelPolicy grpcChannelPolicy,
|
||||
LiquidityBoundsService liquidityBoundsService
|
||||
) {
|
||||
this.grpcGetInfo = grpcGetInfo;
|
||||
this.grpcChannelPolicy = grpcChannelPolicy;
|
||||
this.liquidityBoundsService = liquidityBoundsService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success(HexString preimage, List<PaymentAttemptHop> paymentAttemptHops) {
|
||||
Pubkey startNode = grpcGetInfo.getPubkey();
|
||||
for (PaymentAttemptHop hop : paymentAttemptHops) {
|
||||
Pubkey endNode = getOtherNode(hop, startNode).orElse(null);
|
||||
if (endNode == null) {
|
||||
return;
|
||||
}
|
||||
liquidityBoundsService.markAsMoved(startNode, endNode, hop.amount());
|
||||
startNode = endNode;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failure(List<PaymentAttemptHop> paymentAttemptHops, FailureCode failureCode, int failureSourceIndex) {
|
||||
if (!TEMPORARY_CHANNEL_FAILURE.equals(failureCode)) {
|
||||
return;
|
||||
}
|
||||
Pubkey startNode = grpcGetInfo.getPubkey();
|
||||
for (int i = 0; i < paymentAttemptHops.size(); i++) {
|
||||
PaymentAttemptHop hop = paymentAttemptHops.get(i);
|
||||
Pubkey endNode = getOtherNode(hop, startNode).orElse(null);
|
||||
if (endNode == null) {
|
||||
return;
|
||||
}
|
||||
if (i < failureSourceIndex) {
|
||||
liquidityBoundsService.markAsAvailable(startNode, endNode, hop.amount());
|
||||
} else {
|
||||
liquidityBoundsService.markAsUnavailable(startNode, endNode, hop.amount());
|
||||
return;
|
||||
}
|
||||
startNode = endNode;
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Pubkey> getOtherNode(PaymentAttemptHop hop, Pubkey startNode) {
|
||||
if (hop.targetPubkey().isPresent()) {
|
||||
return hop.targetPubkey();
|
||||
}
|
||||
if (hop.channelId().isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
ChannelId channelId = hop.channelId().get();
|
||||
return grpcChannelPolicy.getOtherPubkey(channelId, startNode);
|
||||
}
|
||||
}
|
||||
@@ -139,4 +139,4 @@ class BalanceServiceTest {
|
||||
when(grpcChannels.getChannel(CHANNEL_ID_2)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL_2));
|
||||
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, LOCAL_OPEN_CHANNEL_2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package de.cotto.lndmanagej.service;
|
||||
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
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.PubkeyFixtures.PUBKEY;
|
||||
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LiquidityBoundsServiceTest {
|
||||
@InjectMocks
|
||||
private LiquidityBoundsService liquidityBoundsService;
|
||||
|
||||
@Mock
|
||||
private MissionControlService missionControlService;
|
||||
|
||||
@Test
|
||||
void getAssumedLiquidityUpperBound_unknown() {
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityUpperBound(PUBKEY, PUBKEY_2)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAssumedLiquidityUpperBound_from_mission_control() {
|
||||
when(missionControlService.getMinimumOfRecentFailures(PUBKEY, PUBKEY_2))
|
||||
.thenReturn(Optional.of(Coins.ofSatoshis(123)));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityUpperBound(PUBKEY, PUBKEY_2))
|
||||
.contains(Coins.ofSatoshis(122));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAssumedLiquidityLowerBound_defaults_to_none() {
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityLowerBound(PUBKEY, PUBKEY_2))
|
||||
.isEqualTo(Coins.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsAvailable() {
|
||||
liquidityBoundsService.markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100_000));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityLowerBound(PUBKEY, PUBKEY_2))
|
||||
.isEqualTo(Coins.ofSatoshis(100_000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsAvailable_uses_maximum() {
|
||||
liquidityBoundsService.markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(120_000));
|
||||
liquidityBoundsService.markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(130_000));
|
||||
liquidityBoundsService.markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(110_000));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityLowerBound(PUBKEY, PUBKEY_2))
|
||||
.isEqualTo(Coins.ofSatoshis(130_000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsMoved_removes_available_coins() {
|
||||
liquidityBoundsService.markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(110_000));
|
||||
liquidityBoundsService.markAsMoved(PUBKEY, PUBKEY_2, Coins.ofSatoshis(50_000));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityLowerBound(PUBKEY, PUBKEY_2))
|
||||
.isEqualTo(Coins.ofSatoshis(60_000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsMoved_below_assumed_available() {
|
||||
liquidityBoundsService.markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(1));
|
||||
liquidityBoundsService.markAsMoved(PUBKEY, PUBKEY_2, Coins.ofSatoshis(50_000));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityLowerBound(PUBKEY, PUBKEY_2))
|
||||
.isEqualTo(Coins.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsUnavailable() {
|
||||
liquidityBoundsService.markAsUnavailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(50_000));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityUpperBound(PUBKEY, PUBKEY_2))
|
||||
.contains(Coins.ofSatoshis(49_999));
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsUnavailable_uses_minimum() {
|
||||
liquidityBoundsService.markAsUnavailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(50_000));
|
||||
liquidityBoundsService.markAsUnavailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(40_000));
|
||||
liquidityBoundsService.markAsUnavailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(60_000));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityUpperBound(PUBKEY, PUBKEY_2))
|
||||
.contains(Coins.ofSatoshis(39_999));
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsUnavailable_with_existing_mission_control_data() {
|
||||
when(missionControlService.getMinimumOfRecentFailures(PUBKEY, PUBKEY_2))
|
||||
.thenReturn(Optional.of(Coins.ofSatoshis(123)));
|
||||
liquidityBoundsService.markAsUnavailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityUpperBound(PUBKEY, PUBKEY_2))
|
||||
.contains(Coins.ofSatoshis(99));
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsAvailable_more_than_upper_bound() {
|
||||
liquidityBoundsService.markAsUnavailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
liquidityBoundsService.markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(120));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityUpperBound(PUBKEY, PUBKEY_2)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsAvailable_below_upper_bound() {
|
||||
liquidityBoundsService.markAsUnavailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
liquidityBoundsService.markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(90));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityUpperBound(PUBKEY, PUBKEY_2))
|
||||
.contains(Coins.ofSatoshis(99));
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAsUnavailable_below_lower_bound() {
|
||||
liquidityBoundsService.markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(120));
|
||||
liquidityBoundsService.markAsUnavailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
assertThat(liquidityBoundsService.getAssumedLiquidityLowerBound(PUBKEY, PUBKEY_2))
|
||||
.isEqualTo(Coins.ofSatoshis(99));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package de.cotto.lndmanagej.service;
|
||||
|
||||
import de.cotto.lndmanagej.grpc.GrpcChannelPolicy;
|
||||
import de.cotto.lndmanagej.grpc.GrpcGetInfo;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.FailureCode;
|
||||
import de.cotto.lndmanagej.model.HexString;
|
||||
import de.cotto.lndmanagej.model.PaymentAttemptHop;
|
||||
import de.cotto.lndmanagej.model.PaymentListener;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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.List;
|
||||
import java.util.Optional;
|
||||
|
||||
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.ChannelIdFixtures.CHANNEL_ID_3;
|
||||
import static de.cotto.lndmanagej.model.FailureCode.TEMPORARY_CHANNEL_FAILURE;
|
||||
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.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LiquidityInformationUpdaterTest {
|
||||
|
||||
private final List<PaymentAttemptHop> hopsWithChannelIdsAndPubkeys = List.of(
|
||||
new PaymentAttemptHop(Optional.of(CHANNEL_ID), Coins.ofSatoshis(100), Optional.of(PUBKEY_2)),
|
||||
new PaymentAttemptHop(Optional.of(CHANNEL_ID_2), Coins.ofSatoshis(90), Optional.of(PUBKEY_3)),
|
||||
new PaymentAttemptHop(Optional.of(CHANNEL_ID_3), Coins.ofSatoshis(80), Optional.of(PUBKEY_4))
|
||||
);
|
||||
|
||||
private final List<PaymentAttemptHop> hopsWithChannelIds = List.of(
|
||||
new PaymentAttemptHop(Optional.of(CHANNEL_ID), Coins.ofSatoshis(100), Optional.empty()),
|
||||
new PaymentAttemptHop(Optional.of(CHANNEL_ID_2), Coins.ofSatoshis(90), Optional.empty()),
|
||||
new PaymentAttemptHop(Optional.of(CHANNEL_ID_3), Coins.ofSatoshis(80), Optional.empty())
|
||||
);
|
||||
|
||||
private final List<PaymentAttemptHop> hopsJustWithAmount = List.of(
|
||||
new PaymentAttemptHop(Optional.empty(), Coins.ofSatoshis(100), Optional.empty()),
|
||||
new PaymentAttemptHop(Optional.empty(), Coins.ofSatoshis(90), Optional.empty()),
|
||||
new PaymentAttemptHop(Optional.empty(), Coins.ofSatoshis(80), Optional.empty())
|
||||
);
|
||||
|
||||
@InjectMocks
|
||||
private LiquidityInformationUpdater liquidityInformationUpdater;
|
||||
|
||||
@Mock
|
||||
private GrpcGetInfo grpcGetInfo;
|
||||
|
||||
@Mock
|
||||
private GrpcChannelPolicy grpcChannelPolicy;
|
||||
|
||||
@Mock
|
||||
private LiquidityBoundsService liquidityBoundsService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY);
|
||||
lenient().when(grpcChannelPolicy.getOtherPubkey(CHANNEL_ID, PUBKEY)).thenReturn(Optional.of(PUBKEY_2));
|
||||
lenient().when(grpcChannelPolicy.getOtherPubkey(CHANNEL_ID_2, PUBKEY_2)).thenReturn(Optional.of(PUBKEY_3));
|
||||
lenient().when(grpcChannelPolicy.getOtherPubkey(CHANNEL_ID_3, PUBKEY_3)).thenReturn(Optional.of(PUBKEY_4));
|
||||
}
|
||||
|
||||
@Test
|
||||
void is_payment_listener() {
|
||||
assertThat(liquidityInformationUpdater).isInstanceOf(PaymentListener.class);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Success {
|
||||
private static final HexString PREIMAGE = new HexString("00");
|
||||
|
||||
// CPD-OFF
|
||||
@Test
|
||||
void success() {
|
||||
liquidityInformationUpdater.success(PREIMAGE, hopsWithChannelIdsAndPubkeys);
|
||||
verify(liquidityBoundsService).markAsMoved(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
verify(liquidityBoundsService).markAsMoved(PUBKEY_2, PUBKEY_3, Coins.ofSatoshis(90));
|
||||
verify(liquidityBoundsService).markAsMoved(PUBKEY_3, PUBKEY_4, Coins.ofSatoshis(80));
|
||||
}
|
||||
|
||||
@Test
|
||||
void success_just_channel_id() {
|
||||
liquidityInformationUpdater.success(PREIMAGE, hopsWithChannelIds);
|
||||
verify(liquidityBoundsService).markAsMoved(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
verify(liquidityBoundsService).markAsMoved(PUBKEY_2, PUBKEY_3, Coins.ofSatoshis(90));
|
||||
verify(liquidityBoundsService).markAsMoved(PUBKEY_3, PUBKEY_4, Coins.ofSatoshis(80));
|
||||
}
|
||||
// CPD-ON
|
||||
|
||||
@Test
|
||||
void success_without_channel_or_pubkey_information() {
|
||||
liquidityInformationUpdater.success(PREIMAGE, hopsJustWithAmount);
|
||||
verifyNoInteractions(liquidityBoundsService);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Failure {
|
||||
@Test
|
||||
void temporary_channel_failure_on_first_hop() {
|
||||
liquidityInformationUpdater.failure(hopsWithChannelIdsAndPubkeys, TEMPORARY_CHANNEL_FAILURE, 0);
|
||||
verify(liquidityBoundsService).markAsUnavailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
verifyNoMoreInteractions(liquidityBoundsService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void temporary_channel_failure_on_second_hop() {
|
||||
liquidityInformationUpdater.failure(hopsWithChannelIdsAndPubkeys, TEMPORARY_CHANNEL_FAILURE, 1);
|
||||
verify(liquidityBoundsService).markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
verify(liquidityBoundsService).markAsUnavailable(PUBKEY_2, PUBKEY_3, Coins.ofSatoshis(90));
|
||||
verifyNoMoreInteractions(liquidityBoundsService);
|
||||
}
|
||||
|
||||
// CPD-OFF
|
||||
@Test
|
||||
void temporary_channel_failure_on_third_hop() {
|
||||
liquidityInformationUpdater.failure(hopsWithChannelIdsAndPubkeys, TEMPORARY_CHANNEL_FAILURE, 2);
|
||||
verify(liquidityBoundsService).markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
verify(liquidityBoundsService).markAsAvailable(PUBKEY_2, PUBKEY_3, Coins.ofSatoshis(90));
|
||||
verify(liquidityBoundsService).markAsUnavailable(PUBKEY_3, PUBKEY_4, Coins.ofSatoshis(80));
|
||||
}
|
||||
|
||||
@Test
|
||||
void temporary_channel_failure_on_hop_that_does_not_exist() {
|
||||
liquidityInformationUpdater.failure(hopsWithChannelIdsAndPubkeys, TEMPORARY_CHANNEL_FAILURE, 99);
|
||||
verify(liquidityBoundsService).markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
verify(liquidityBoundsService).markAsAvailable(PUBKEY_2, PUBKEY_3, Coins.ofSatoshis(90));
|
||||
verify(liquidityBoundsService).markAsAvailable(PUBKEY_3, PUBKEY_4, Coins.ofSatoshis(80));
|
||||
}
|
||||
|
||||
@Test
|
||||
void temporary_channel_just_channel_information() {
|
||||
liquidityInformationUpdater.failure(hopsWithChannelIds, TEMPORARY_CHANNEL_FAILURE, 2);
|
||||
verify(liquidityBoundsService).markAsAvailable(PUBKEY, PUBKEY_2, Coins.ofSatoshis(100));
|
||||
verify(liquidityBoundsService).markAsAvailable(PUBKEY_2, PUBKEY_3, Coins.ofSatoshis(90));
|
||||
verify(liquidityBoundsService).markAsUnavailable(PUBKEY_3, PUBKEY_4, Coins.ofSatoshis(80));
|
||||
}
|
||||
// CPD-ON
|
||||
|
||||
@Test
|
||||
void temporary_channel_failure_without_channel_or_pubkey_information() {
|
||||
liquidityInformationUpdater.failure(hopsJustWithAmount, TEMPORARY_CHANNEL_FAILURE, 2);
|
||||
verifyNoInteractions(liquidityBoundsService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknown_failure_code() {
|
||||
liquidityInformationUpdater.failure(hopsWithChannelIdsAndPubkeys, new FailureCode(99), 2);
|
||||
verifyNoInteractions(liquidityBoundsService);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,4 +127,4 @@ class MissionControlServiceTest {
|
||||
Set<MissionControlEntry> set = Arrays.stream(entries).collect(toCollection(LinkedHashSet::new));
|
||||
when(grpcMissionControl.getEntries()).thenReturn(Optional.of(set));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.cotto.lndmanagej.grpc.middleware;
|
||||
import com.google.protobuf.ByteString;
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.FailureCode;
|
||||
import de.cotto.lndmanagej.model.HexString;
|
||||
import de.cotto.lndmanagej.model.PaymentAttemptHop;
|
||||
import de.cotto.lndmanagej.model.PaymentListener;
|
||||
@@ -51,7 +52,7 @@ public class SendToRouteListener extends RequestResponseListener<SendToRouteRequ
|
||||
.toList();
|
||||
if (preimage.isEmpty()) {
|
||||
Failure failure = response.getFailure();
|
||||
int failureCode = failure.getCodeValue();
|
||||
FailureCode failureCode = new FailureCode(failure.getCodeValue());
|
||||
int failureSourceIndex = failure.getFailureSourceIndex();
|
||||
paymentListeners.forEach(
|
||||
paymentListener -> paymentListener.failure(paymentAttemptHops, failureCode, failureSourceIndex)
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.cotto.lndmanagej.grpc.middleware;
|
||||
import com.google.protobuf.ByteString;
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.FailureCode;
|
||||
import de.cotto.lndmanagej.model.HexString;
|
||||
import de.cotto.lndmanagej.model.PaymentAttemptHop;
|
||||
import de.cotto.lndmanagej.model.PaymentListener;
|
||||
@@ -125,7 +126,7 @@ class SendToRouteListenerTest {
|
||||
.setFailureSourceIndex(3)
|
||||
.build())
|
||||
.build(), REQUEST_ID);
|
||||
verify(paymentListener).failure(paymentAttemptHops, 15, 3);
|
||||
verify(paymentListener).failure(paymentAttemptHops, FailureCode.TEMPORARY_CHANNEL_FAILURE, 3);
|
||||
verifyNoMoreInteractions(paymentListener);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.cotto.lndmanagej.model;
|
||||
|
||||
public record FailureCode(int code) {
|
||||
public static final FailureCode TEMPORARY_CHANNEL_FAILURE = new FailureCode(15);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.cotto.lndmanagej.model;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Optional;
|
||||
|
||||
public class LiquidityBounds {
|
||||
private static final Duration MAX_AGE = Duration.of(1, ChronoUnit.HOURS);
|
||||
private Instant lowerBoundLastUpdate;
|
||||
private Instant upperBoundLastUpdate;
|
||||
|
||||
private Coins lowerBound;
|
||||
@CheckForNull
|
||||
private Coins upperBound;
|
||||
|
||||
public LiquidityBounds() {
|
||||
lowerBound = Coins.NONE;
|
||||
lowerBoundLastUpdate = Instant.now();
|
||||
upperBoundLastUpdate = Instant.now();
|
||||
}
|
||||
|
||||
public void move(Coins amount) {
|
||||
lowerBound = lowerBound.subtract(amount).maximum(Coins.NONE);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.NullAssignment")
|
||||
public void available(Coins amount) {
|
||||
resetOldLowerBound();
|
||||
lowerBoundLastUpdate = Instant.now();
|
||||
lowerBound = lowerBound.maximum(amount);
|
||||
if (upperBound != null && lowerBound.compareTo(upperBound) >= 0) {
|
||||
upperBound = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void unavailable(Coins amount) {
|
||||
resetOldUpperBound();
|
||||
upperBoundLastUpdate = Instant.now();
|
||||
Coins newUpperBound = amount.subtract(Coins.ofSatoshis(1));
|
||||
if (upperBound == null) {
|
||||
upperBound = newUpperBound;
|
||||
} else {
|
||||
upperBound = upperBound.minimum(newUpperBound);
|
||||
}
|
||||
lowerBound = lowerBound.minimum(upperBound);
|
||||
}
|
||||
|
||||
public Coins getLowerBound() {
|
||||
resetOldLowerBound();
|
||||
return lowerBound;
|
||||
}
|
||||
|
||||
public Optional<Coins> getUpperBound() {
|
||||
resetOldUpperBound();
|
||||
return Optional.ofNullable(upperBound);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.NullAssignment")
|
||||
private void resetOldUpperBound() {
|
||||
if (upperBoundLastUpdate.isBefore(Instant.now().minus(MAX_AGE))) {
|
||||
upperBound = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetOldLowerBound() {
|
||||
if (lowerBoundLastUpdate.isBefore(Instant.now().minus(MAX_AGE))) {
|
||||
lowerBound = Coins.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setLowerBoundLastUpdate(Instant lowerBoundLastUpdate) {
|
||||
this.lowerBoundLastUpdate = lowerBoundLastUpdate;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setUpperBoundLastUpdate(Instant upperBoundLastUpdate) {
|
||||
this.upperBoundLastUpdate = upperBoundLastUpdate;
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,5 @@ import java.util.List;
|
||||
public interface PaymentListener {
|
||||
void success(HexString preimage, List<PaymentAttemptHop> paymentAttemptHops);
|
||||
|
||||
void failure(List<PaymentAttemptHop> paymentAttemptHops, int failureCode, int failureSourceIndex);
|
||||
void failure(List<PaymentAttemptHop> paymentAttemptHops, FailureCode failureCode, int failureSourceIndex);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.cotto.lndmanagej.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static de.cotto.lndmanagej.model.FailureCode.TEMPORARY_CHANNEL_FAILURE;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FailureCodeTest {
|
||||
@Test
|
||||
void temporaryChannelFailure() {
|
||||
assertThat(TEMPORARY_CHANNEL_FAILURE.code()).isEqualTo(15);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package de.cotto.lndmanagej.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LiquidityBoundsTest {
|
||||
private final LiquidityBounds liquidityBounds = new LiquidityBounds();
|
||||
|
||||
@Test
|
||||
void getLowerBound_initially_zero() {
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(Coins.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLowerBound_after_available_update() {
|
||||
Coins amount = Coins.ofSatoshis(100);
|
||||
liquidityBounds.available(amount);
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(amount);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLowerBound_two_available_updates() {
|
||||
Coins amount1 = Coins.ofSatoshis(100);
|
||||
Coins amount2 = Coins.ofSatoshis(200);
|
||||
liquidityBounds.available(amount1);
|
||||
liquidityBounds.available(amount2);
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(amount2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLowerBound_two_available_updates_reversed() {
|
||||
Coins amount1 = Coins.ofSatoshis(200);
|
||||
Coins amount2 = Coins.ofSatoshis(100);
|
||||
liquidityBounds.available(amount1);
|
||||
liquidityBounds.available(amount2);
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(amount1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUpperBound_initially_unknown() {
|
||||
assertThat(liquidityBounds.getUpperBound()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUpperBound_after_unavailable_update() {
|
||||
Coins amount = Coins.ofSatoshis(200);
|
||||
liquidityBounds.unavailable(amount);
|
||||
assertThat(liquidityBounds.getUpperBound()).contains(oneSatLessThan(amount));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUpperBound_after_two_unavailable_updates() {
|
||||
Coins amount1 = Coins.ofSatoshis(200);
|
||||
Coins amount2 = Coins.ofSatoshis(100);
|
||||
liquidityBounds.unavailable(amount1);
|
||||
liquidityBounds.unavailable(amount2);
|
||||
assertThat(liquidityBounds.getUpperBound()).contains(oneSatLessThan(amount2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUpperBound_after_unavailable_updates_reversed() {
|
||||
Coins amount1 = Coins.ofSatoshis(100);
|
||||
Coins amount2 = Coins.ofSatoshis(200);
|
||||
liquidityBounds.unavailable(amount1);
|
||||
liquidityBounds.unavailable(amount2);
|
||||
assertThat(liquidityBounds.getUpperBound()).contains(oneSatLessThan(amount1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void available_update_invalidates_lower_upper_bound() {
|
||||
Coins amount1 = Coins.ofSatoshis(100);
|
||||
Coins amount2 = oneSatLessThan(amount1);
|
||||
liquidityBounds.unavailable(amount1);
|
||||
liquidityBounds.available(amount2);
|
||||
assertThat(liquidityBounds.getUpperBound()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void available_update_keeps_higher_upper_bound() {
|
||||
Coins amount1 = Coins.ofSatoshis(300);
|
||||
Coins amount2 = Coins.ofSatoshis(298);
|
||||
liquidityBounds.unavailable(amount1);
|
||||
liquidityBounds.available(amount2);
|
||||
assertThat(liquidityBounds.getUpperBound()).contains(oneSatLessThan(amount1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unavailable_update_updates_lower_bound() {
|
||||
Coins amount1 = Coins.ofSatoshis(300);
|
||||
Coins amount2 = Coins.ofSatoshis(301);
|
||||
liquidityBounds.available(amount1);
|
||||
liquidityBounds.unavailable(amount2);
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(oneSatLessThan(amount2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unavailable_what_is_assumed_to_be_available() {
|
||||
Coins amount1 = Coins.ofSatoshis(300);
|
||||
Coins amount2 = Coins.ofSatoshis(300);
|
||||
liquidityBounds.available(amount1);
|
||||
liquidityBounds.unavailable(amount2);
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(oneSatLessThan(amount2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unavailable_more_than_available() {
|
||||
Coins amount1 = Coins.ofSatoshis(300);
|
||||
Coins amount2 = Coins.ofSatoshis(500);
|
||||
liquidityBounds.available(amount1);
|
||||
liquidityBounds.unavailable(amount2);
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(amount1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void move_updates_lower_bound() {
|
||||
Coins amount1 = Coins.ofSatoshis(100);
|
||||
Coins amount2 = Coins.ofSatoshis(60);
|
||||
liquidityBounds.available(amount1);
|
||||
liquidityBounds.move(amount2);
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(Coins.ofSatoshis(40));
|
||||
}
|
||||
|
||||
@Test
|
||||
void move_more_than_lower_bound() {
|
||||
Coins amount1 = Coins.ofSatoshis(100);
|
||||
Coins amount2 = Coins.ofSatoshis(200);
|
||||
liquidityBounds.available(amount1);
|
||||
liquidityBounds.move(amount2);
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(Coins.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void available_forgets_lower_bound_after_one_hour() {
|
||||
Coins higherButOld = Coins.ofSatoshis(200);
|
||||
Coins lowerMoreRecent = Coins.ofSatoshis(100);
|
||||
liquidityBounds.available(higherButOld);
|
||||
liquidityBounds.setLowerBoundLastUpdate(oneHourAgo());
|
||||
|
||||
liquidityBounds.available(lowerMoreRecent);
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(lowerMoreRecent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void available_updates_last_update_of_lower_bound() {
|
||||
liquidityBounds.setLowerBoundLastUpdate(oneHourAgo());
|
||||
|
||||
Coins lowerBound = Coins.ofSatoshis(50);
|
||||
liquidityBounds.available(lowerBound);
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(lowerBound);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unavailable_forgets_upper_bound_after_one_hour() {
|
||||
liquidityBounds.unavailable(Coins.ofSatoshis(100));
|
||||
liquidityBounds.setUpperBoundLastUpdate(oneHourAgo());
|
||||
|
||||
Coins moreRecentAmount = Coins.ofSatoshis(200);
|
||||
liquidityBounds.unavailable(moreRecentAmount);
|
||||
assertThat(liquidityBounds.getUpperBound()).contains(oneSatLessThan(moreRecentAmount));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unavailable_updates_last_update_of_upper_bound() {
|
||||
liquidityBounds.setUpperBoundLastUpdate(oneHourAgo());
|
||||
|
||||
Coins moreRecentValue = Coins.ofSatoshis(100);
|
||||
liquidityBounds.unavailable(moreRecentValue);
|
||||
assertThat(liquidityBounds.getUpperBound()).contains(oneSatLessThan(moreRecentValue));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLowerBound_does_not_return_old_value() {
|
||||
liquidityBounds.available(Coins.ofSatoshis(100));
|
||||
liquidityBounds.setLowerBoundLastUpdate(oneHourAgo());
|
||||
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(Coins.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLowerBound_value_is_kept_for_old_upper_bound() {
|
||||
Coins amount = Coins.ofSatoshis(100);
|
||||
liquidityBounds.available(amount);
|
||||
liquidityBounds.setUpperBoundLastUpdate(oneHourAgo());
|
||||
|
||||
assertThat(liquidityBounds.getLowerBound()).isEqualTo(amount);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUpperBound_does_not_return_old_value() {
|
||||
liquidityBounds.unavailable(Coins.ofSatoshis(100));
|
||||
liquidityBounds.setUpperBoundLastUpdate(oneHourAgo());
|
||||
|
||||
assertThat(liquidityBounds.getUpperBound()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUpperBound_value_is_kept_for_old_lower_bound() {
|
||||
Coins amount = Coins.ofSatoshis(100);
|
||||
liquidityBounds.unavailable(amount);
|
||||
liquidityBounds.setLowerBoundLastUpdate(oneHourAgo());
|
||||
|
||||
assertThat(liquidityBounds.getUpperBound()).contains(oneSatLessThan(amount));
|
||||
}
|
||||
|
||||
private Instant oneHourAgo() {
|
||||
return Instant.now().minus(1, ChronoUnit.HOURS);
|
||||
}
|
||||
|
||||
private Coins oneSatLessThan(Coins amount) {
|
||||
return amount.subtract(Coins.ofSatoshis(1));
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import de.cotto.lndmanagej.pickhardtpayments.model.EdgeWithLiquidityInformation;
|
||||
import de.cotto.lndmanagej.pickhardtpayments.model.EdgesWithLiquidityInformation;
|
||||
import de.cotto.lndmanagej.service.BalanceService;
|
||||
import de.cotto.lndmanagej.service.ChannelService;
|
||||
import de.cotto.lndmanagej.service.MissionControlService;
|
||||
import de.cotto.lndmanagej.service.LiquidityBoundsService;
|
||||
import de.cotto.lndmanagej.service.NodeService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -36,7 +36,7 @@ public class EdgeComputation {
|
||||
private final ChannelService channelService;
|
||||
private final NodeService nodeService;
|
||||
private final BalanceService balanceService;
|
||||
private final MissionControlService missionControlService;
|
||||
private final LiquidityBoundsService liquidityBoundsService;
|
||||
private final LoadingCache<Object, EdgesWithLiquidityInformation> cache = new CacheBuilder()
|
||||
.withExpiry(Duration.ofSeconds(10))
|
||||
.withRefresh(Duration.ofSeconds(5))
|
||||
@@ -48,14 +48,14 @@ public class EdgeComputation {
|
||||
ChannelService channelService,
|
||||
NodeService nodeService,
|
||||
BalanceService balanceService,
|
||||
MissionControlService missionControlService
|
||||
LiquidityBoundsService liquidityBoundsService
|
||||
) {
|
||||
this.grpcGraph = grpcGraph;
|
||||
this.grpcGetInfo = grpcGetInfo;
|
||||
this.channelService = channelService;
|
||||
this.nodeService = nodeService;
|
||||
this.balanceService = balanceService;
|
||||
this.missionControlService = missionControlService;
|
||||
this.liquidityBoundsService = liquidityBoundsService;
|
||||
}
|
||||
|
||||
public EdgesWithLiquidityInformation getEdges() {
|
||||
@@ -70,8 +70,9 @@ public class EdgeComputation {
|
||||
private EdgeWithLiquidityInformation getEdgeWithLiquidityInformation(Edge edge, Pubkey ownPubkey) {
|
||||
Coins knownLiquidity = getKnownLiquidity(edge, ownPubkey).orElse(null);
|
||||
if (knownLiquidity == null) {
|
||||
Coins availableLiquidityUpperBound = getAvailableLiquidityUpperBound(edge);
|
||||
return EdgeWithLiquidityInformation.forUpperBound(edge, availableLiquidityUpperBound);
|
||||
Coins lowerBound = liquidityBoundsService.getAssumedLiquidityLowerBound(edge.startNode(), edge.endNode());
|
||||
Coins upperBound = getAvailableLiquidityUpperBound(edge);
|
||||
return EdgeWithLiquidityInformation.forLowerAndUpperBound(edge, lowerBound, upperBound);
|
||||
}
|
||||
return EdgeWithLiquidityInformation.forKnownLiquidity(edge, knownLiquidity);
|
||||
}
|
||||
@@ -137,13 +138,7 @@ public class EdgeComputation {
|
||||
private Coins getAvailableLiquidityUpperBound(Edge edge) {
|
||||
Pubkey source = edge.startNode();
|
||||
Pubkey target = edge.endNode();
|
||||
Coins failureAmount = missionControlService.getMinimumOfRecentFailures(source, target).orElse(null);
|
||||
if (failureAmount == null) {
|
||||
return edge.capacity();
|
||||
}
|
||||
long satsCapacity = edge.capacity().satoshis();
|
||||
long satsNotAvailable = failureAmount.milliSatoshis() / 1_000;
|
||||
long satsAvailable = Math.max(Math.min(satsNotAvailable - 1, satsCapacity), 0);
|
||||
return Coins.ofSatoshis(satsAvailable);
|
||||
Coins upperBound = liquidityBoundsService.getAssumedLiquidityUpperBound(source, target).orElse(null);
|
||||
return edge.capacity().minimum(upperBound);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import de.cotto.lndmanagej.model.Pubkey;
|
||||
import de.cotto.lndmanagej.pickhardtpayments.model.EdgeWithLiquidityInformation;
|
||||
import de.cotto.lndmanagej.service.BalanceService;
|
||||
import de.cotto.lndmanagej.service.ChannelService;
|
||||
import de.cotto.lndmanagej.service.MissionControlService;
|
||||
import de.cotto.lndmanagej.service.LiquidityBoundsService;
|
||||
import de.cotto.lndmanagej.service.NodeService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -57,16 +57,16 @@ class EdgeComputationTest {
|
||||
private GrpcGraph grpcGraph;
|
||||
|
||||
@Mock
|
||||
private MissionControlService missionControlService;
|
||||
private NodeService nodeService;
|
||||
|
||||
@Mock
|
||||
private NodeService nodeService;
|
||||
private LiquidityBoundsService liquidityBoundsService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4);
|
||||
lenient().when(missionControlService.getMinimumOfRecentFailures(any(), any())).thenReturn(Optional.empty());
|
||||
lenient().when(nodeService.getNode(any())).thenReturn(NODE_PEER);
|
||||
lenient().when(liquidityBoundsService.getAssumedLiquidityLowerBound(any(), any())).thenReturn(Coins.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,12 +160,13 @@ class EdgeComputationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void adds_upper_bound_from_mission_control() {
|
||||
void adds_upper_bound_from_liquidity_bounds_service() {
|
||||
mockEdge();
|
||||
when(missionControlService.getMinimumOfRecentFailures(EDGE.startNode(), EDGE.endNode()))
|
||||
.thenReturn(Optional.of(Coins.ofSatoshis(100)));
|
||||
Coins upperBound = Coins.ofSatoshis(100);
|
||||
when(liquidityBoundsService.getAssumedLiquidityUpperBound(EDGE.startNode(), EDGE.endNode()))
|
||||
.thenReturn(Optional.of(upperBound));
|
||||
assertThat(edgeComputation.getEdges().edges())
|
||||
.contains(EdgeWithLiquidityInformation.forUpperBound(EDGE, Coins.ofSatoshis(99)));
|
||||
.contains(EdgeWithLiquidityInformation.forUpperBound(EDGE, upperBound));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -203,16 +204,48 @@ class EdgeComputationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEdgeWithLiquidityInformation_with_data_from_mission_control() {
|
||||
void getEdgeWithLiquidityInformation_with_upper_bound() {
|
||||
when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4);
|
||||
Coins recentFailureAmount = Coins.ofSatoshis(456);
|
||||
Coins upperBound = Coins.ofSatoshis(455);
|
||||
when(missionControlService.getMinimumOfRecentFailures(EDGE.startNode(), EDGE.endNode()))
|
||||
.thenReturn(Optional.of(recentFailureAmount));
|
||||
when(liquidityBoundsService.getAssumedLiquidityUpperBound(EDGE.startNode(), EDGE.endNode()))
|
||||
.thenReturn(Optional.of(upperBound));
|
||||
assertThat(edgeComputation.getEdgeWithLiquidityInformation(EDGE))
|
||||
.isEqualTo(EdgeWithLiquidityInformation.forUpperBound(EDGE, upperBound));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEdgeWithLiquidityInformation_with_upper_bound_above_capacity() {
|
||||
when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4);
|
||||
Coins upperBound = EDGE.capacity().add(Coins.ofSatoshis(1));
|
||||
when(liquidityBoundsService.getAssumedLiquidityUpperBound(EDGE.startNode(), EDGE.endNode()))
|
||||
.thenReturn(Optional.of(upperBound));
|
||||
assertThat(edgeComputation.getEdgeWithLiquidityInformation(EDGE))
|
||||
.isEqualTo(EdgeWithLiquidityInformation.forUpperBound(EDGE, EDGE.capacity()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEdgeWithLiquidityInformation_with_lower_bound() {
|
||||
when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4);
|
||||
Coins lowerBound = Coins.ofSatoshis(455);
|
||||
when(liquidityBoundsService.getAssumedLiquidityLowerBound(EDGE.startNode(), EDGE.endNode()))
|
||||
.thenReturn(lowerBound);
|
||||
assertThat(edgeComputation.getEdgeWithLiquidityInformation(EDGE))
|
||||
.isEqualTo(EdgeWithLiquidityInformation.forLowerBound(EDGE, lowerBound));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEdgeWithLiquidityInformation_with_lower_and_upper_bound() {
|
||||
when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4);
|
||||
Coins lowerBound = Coins.ofSatoshis(100);
|
||||
Coins upperBound = Coins.ofSatoshis(455);
|
||||
when(liquidityBoundsService.getAssumedLiquidityLowerBound(EDGE.startNode(), EDGE.endNode()))
|
||||
.thenReturn(lowerBound);
|
||||
when(liquidityBoundsService.getAssumedLiquidityUpperBound(EDGE.startNode(), EDGE.endNode()))
|
||||
.thenReturn(Optional.of(upperBound));
|
||||
assertThat(edgeComputation.getEdgeWithLiquidityInformation(EDGE))
|
||||
.isEqualTo(EdgeWithLiquidityInformation.forLowerAndUpperBound(EDGE, lowerBound, upperBound));
|
||||
}
|
||||
|
||||
private void mockEdge() {
|
||||
DirectedChannelEdge edge = new DirectedChannelEdge(CHANNEL_ID, CAPACITY, PUBKEY, PUBKEY_2, POLICY_1);
|
||||
when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(edge)));
|
||||
|
||||
Reference in New Issue
Block a user