diff --git a/backend/src/main/java/de/cotto/lndmanagej/service/OffChainCostService.java b/backend/src/main/java/de/cotto/lndmanagej/service/OffChainCostService.java index 3a9af3c3..d0dc35f8 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/OffChainCostService.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/OffChainCostService.java @@ -1,67 +1,54 @@ package de.cotto.lndmanagej.service; import com.codahale.metrics.annotation.Timed; -import de.cotto.lndmanagej.model.Channel; import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.OffChainCosts; import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.model.SelfPayment; import org.springframework.stereotype.Component; +import java.util.Set; + @Component public class OffChainCostService { - private final SelfPaymentsService selfPaymentsService; - private final ChannelService channelService; + private final RebalanceService rebalanceService; - public OffChainCostService(SelfPaymentsService selfPaymentsService, ChannelService channelService) { - this.selfPaymentsService = selfPaymentsService; - this.channelService = channelService; + public OffChainCostService(RebalanceService rebalanceService) { + this.rebalanceService = rebalanceService; + } + + @Timed + public OffChainCosts getOffChainCostsForPeer(Pubkey pubkey) { + return new OffChainCosts( + getRebalanceSourceCostsForPeer(pubkey), + getRebalanceTargetCostsForPeer(pubkey) + ); } @Timed public Coins getRebalanceSourceCostsForChannel(ChannelId channelId) { - return selfPaymentsService.getSelfPaymentsFromChannel(channelId).stream() - .filter(selfPayment -> memoMentionsChannel(selfPayment, channelId)) - .map(SelfPayment::fees) - .reduce(Coins.NONE, Coins::add); + return getSumOfFees(rebalanceService.getRebalancesFromChannel(channelId)); } @Timed public Coins getRebalanceSourceCostsForPeer(Pubkey pubkey) { - return channelService.getAllChannelsWith(pubkey).parallelStream() - .map(Channel::getId) - .map(this::getRebalanceSourceCostsForChannel) - .reduce(Coins.NONE, Coins::add); + return getSumOfFees(rebalanceService.getRebalancesFromPeer(pubkey)); } @Timed public Coins getRebalanceTargetCostsForChannel(ChannelId channelId) { - return selfPaymentsService.getSelfPaymentsToChannel(channelId).stream() - .filter(this::memoDoesNotMentionFirstHopChannel) - .map(SelfPayment::fees) - .reduce(Coins.NONE, Coins::add); + return getSumOfFees(rebalanceService.getRebalancesToChannel(channelId)); } @Timed public Coins getRebalanceTargetCostsForPeer(Pubkey pubkey) { - return channelService.getAllChannelsWith(pubkey).parallelStream() - .map(Channel::getId) - .map(this::getRebalanceTargetCostsForChannel) + return getSumOfFees(rebalanceService.getRebalancesToPeer(pubkey)); + } + + private Coins getSumOfFees(Set selfPayments) { + return selfPayments.stream() + .map(SelfPayment::fees) .reduce(Coins.NONE, Coins::add); } - - private boolean memoMentionsChannel(SelfPayment selfPayment, ChannelId expectedChannel) { - String memo = selfPayment.memo(); - return memo.contains(String.valueOf(expectedChannel.getShortChannelId())) - || memo.contains(expectedChannel.getCompactForm()) - || memo.contains(expectedChannel.getCompactFormLnd()); - } - - private boolean memoDoesNotMentionFirstHopChannel(SelfPayment selfPayment) { - ChannelId firstChannel = selfPayment.firstChannel().orElse(null); - if (firstChannel == null) { - return true; - } - return !memoMentionsChannel(selfPayment, firstChannel); - } } diff --git a/backend/src/main/java/de/cotto/lndmanagej/service/RebalanceService.java b/backend/src/main/java/de/cotto/lndmanagej/service/RebalanceService.java new file mode 100644 index 00000000..2598eab5 --- /dev/null +++ b/backend/src/main/java/de/cotto/lndmanagej/service/RebalanceService.java @@ -0,0 +1,70 @@ +package de.cotto.lndmanagej.service; + +import com.codahale.metrics.annotation.Timed; +import de.cotto.lndmanagej.model.Channel; +import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.Pubkey; +import de.cotto.lndmanagej.model.SelfPayment; +import org.springframework.stereotype.Component; + +import java.util.Set; + +import static java.util.stream.Collectors.toSet; + +@Component +public class RebalanceService { + private final SelfPaymentsService selfPaymentsService; + private final ChannelService channelService; + + public RebalanceService(SelfPaymentsService selfPaymentsService, ChannelService channelService) { + this.selfPaymentsService = selfPaymentsService; + this.channelService = channelService; + } + + @Timed + public Set getRebalancesFromChannel(ChannelId channelId) { + return selfPaymentsService.getSelfPaymentsFromChannel(channelId).stream() + .filter(selfPayment -> memoMentionsChannel(selfPayment, channelId)) + .collect(toSet()); + } + + @Timed + public Set getRebalancesFromPeer(Pubkey pubkey) { + return channelService.getAllChannelsWith(pubkey).parallelStream() + .map(Channel::getId) + .map(this::getRebalancesFromChannel) + .flatMap(Set::stream) + .collect(toSet()); + } + + @Timed + public Set getRebalancesToChannel(ChannelId channelId) { + return selfPaymentsService.getSelfPaymentsToChannel(channelId).stream() + .filter(this::memoDoesNotMentionFirstHopChannel) + .collect(toSet()); + } + + @Timed + public Set getRebalancesToPeer(Pubkey pubkey) { + return channelService.getAllChannelsWith(pubkey).parallelStream() + .map(Channel::getId) + .map(this::getRebalancesToChannel) + .flatMap(Set::stream) + .collect(toSet()); + } + + private boolean memoMentionsChannel(SelfPayment selfPayment, ChannelId expectedChannel) { + String memo = selfPayment.memo(); + return memo.contains(String.valueOf(expectedChannel.getShortChannelId())) + || memo.contains(expectedChannel.getCompactForm()) + || memo.contains(expectedChannel.getCompactFormLnd()); + } + + private boolean memoDoesNotMentionFirstHopChannel(SelfPayment selfPayment) { + ChannelId firstChannel = selfPayment.firstChannel().orElse(null); + if (firstChannel == null) { + return true; + } + return !memoMentionsChannel(selfPayment, firstChannel); + } +} diff --git a/backend/src/test/java/de/cotto/lndmanagej/service/OffChainCostServiceTest.java b/backend/src/test/java/de/cotto/lndmanagej/service/OffChainCostServiceTest.java index cc6ca2a0..41e50d70 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/OffChainCostServiceTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/OffChainCostServiceTest.java @@ -1,53 +1,52 @@ package de.cotto.lndmanagej.service; -import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.Coins; -import de.cotto.lndmanagej.model.Payment; -import de.cotto.lndmanagej.model.PaymentHop; -import de.cotto.lndmanagej.model.PaymentRoute; -import de.cotto.lndmanagej.model.SelfPayment; -import de.cotto.lndmanagej.model.SettledInvoice; +import de.cotto.lndmanagej.model.OffChainCosts; 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 java.util.Set; 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.CoopClosedChannelFixtures.CLOSED_CHANNEL_2; -import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL; -import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_CREATION_DATE_TIME; -import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_FEES; -import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_HASH; -import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_INDEX; -import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_VALUE; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; -import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.ADD_INDEX; -import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.AMOUNT_PAID; -import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.HASH; -import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.SETTLE_DATE; -import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.SETTLE_INDEX; +import static de.cotto.lndmanagej.model.SelfPaymentFixtures.SELF_PAYMENT; +import static de.cotto.lndmanagej.model.SelfPaymentFixtures.SELF_PAYMENT_2; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class OffChainCostServiceTest { - private static final Coins COST_FOR_TWO_SELF_PAYMENTS = PAYMENT_FEES.add(PAYMENT_FEES); + private static final Coins COST_FOR_TWO_SELF_PAYMENTS = SELF_PAYMENT.fees().add(SELF_PAYMENT_2.fees()); @InjectMocks private OffChainCostService offChainCostService; @Mock - private SelfPaymentsService selfPaymentsService; + private RebalanceService rebalanceService; - @Mock - private ChannelService channelService; + @Test + void getOffChainCostsForPeer_source() { + when(rebalanceService.getRebalancesFromPeer(PUBKEY)).thenReturn(Set.of(SELF_PAYMENT, SELF_PAYMENT_2)); + OffChainCosts expected = new OffChainCosts( + COST_FOR_TWO_SELF_PAYMENTS, + Coins.NONE + ); + assertThat(offChainCostService.getOffChainCostsForPeer(PUBKEY)).isEqualTo(expected); + } + + @Test + void getOffChainCostsForPeer_target() { + when(rebalanceService.getRebalancesToPeer(PUBKEY)).thenReturn(Set.of(SELF_PAYMENT, SELF_PAYMENT_2)); + OffChainCosts expected = new OffChainCosts( + Coins.NONE, + COST_FOR_TWO_SELF_PAYMENTS + ); + assertThat(offChainCostService.getOffChainCostsForPeer(PUBKEY)).isEqualTo(expected); + } @Test void getRebalanceSourceCostsForChannel_no_self_payments() { @@ -55,40 +54,15 @@ class OffChainCostServiceTest { } @Test - void getRebalanceSourceCostsForChannel_short_channel_id_in_memo() { - mockSelfPaymentsFromChannel("rebalance from " + CHANNEL_ID.getShortChannelId()); + void getRebalanceSourceCostsForChannel() { + when(rebalanceService.getRebalancesFromChannel(CHANNEL_ID)).thenReturn(Set.of(SELF_PAYMENT, SELF_PAYMENT_2)); assertThat(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID)) .isEqualTo(COST_FOR_TWO_SELF_PAYMENTS); } - @Test - void getRebalanceSourceCostsForChannel_compact_channel_id_in_memo() { - mockSelfPaymentsFromChannel("rebalance from " + CHANNEL_ID.getCompactForm()); - assertThat(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID)) - .isEqualTo(COST_FOR_TWO_SELF_PAYMENTS); - } - - @Test - void getRebalanceSourceCostsForChannel_compact_lnd_channel_id_in_memo() { - mockSelfPaymentsFromChannel("rebalance from " + CHANNEL_ID.getCompactFormLnd()); - assertThat(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID)) - .isEqualTo(COST_FOR_TWO_SELF_PAYMENTS); - } - - @Test - void getRebalanceSourceCostsForChannel_id_not_in_memo() { - mockSelfPaymentsFromChannel("something"); - assertThat(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID)) - .isEqualTo(Coins.NONE); - } - @Test void getRebalanceSourceCostsForPeer() { - ChannelId id1 = LOCAL_OPEN_CHANNEL.getId(); - ChannelId id2 = CLOSED_CHANNEL_2.getId(); - when(channelService.getAllChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, CLOSED_CHANNEL_2)); - when(selfPaymentsService.getSelfPaymentsFromChannel(id1)).thenReturn(List.of(getSelfPayment(id1.toString()))); - when(selfPaymentsService.getSelfPaymentsFromChannel(id2)).thenReturn(List.of(getSelfPayment(id2.toString()))); + when(rebalanceService.getRebalancesFromPeer(PUBKEY)).thenReturn(Set.of(SELF_PAYMENT, SELF_PAYMENT_2)); assertThat(offChainCostService.getRebalanceSourceCostsForPeer(PUBKEY)) .isEqualTo(COST_FOR_TWO_SELF_PAYMENTS); } @@ -99,115 +73,17 @@ class OffChainCostServiceTest { } @Test - void getRebalanceTargetCostsForChannel_short_channel_id_in_memo() { - mockSelfPaymentsToChannel("foo bar " + CHANNEL_ID_2.getShortChannelId() + "!"); + void getRebalanceTargetCostsForChannel() { + when(rebalanceService.getRebalancesToChannel(CHANNEL_ID_2)).thenReturn(Set.of(SELF_PAYMENT, SELF_PAYMENT_2)); assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2)) .isEqualTo(COST_FOR_TWO_SELF_PAYMENTS); } - @Test - void getRebalanceTargetCostsForChannel_compact_channel_id_in_memo() { - mockSelfPaymentsToChannel("something something " + CHANNEL_ID_2.getCompactForm()); - assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2)) - .isEqualTo(COST_FOR_TWO_SELF_PAYMENTS); - } - - @Test - void getRebalanceTargetCostsForChannel_compact_lnd_channel_id_in_memo() { - mockSelfPaymentsToChannel(CHANNEL_ID_2.getCompactFormLnd()); - assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2)) - .isEqualTo(COST_FOR_TWO_SELF_PAYMENTS); - } - - @Test - void getRebalanceTargetCostsForChannel_id_not_in_memo() { - mockSelfPaymentsToChannel("something"); - assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2)) - .isEqualTo(COST_FOR_TWO_SELF_PAYMENTS); - } - - @Test - void getRebalanceTargetCostsForChannel_other_channel_id_in_memo() { - mockSelfPaymentsToChannel("rebalance to " + CHANNEL_ID_3); - assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2)) - .isEqualTo(COST_FOR_TWO_SELF_PAYMENTS); - } - - @Test - void getRebalanceTargetCostsForChannel_source_channel_id_in_memo() { - mockSelfPaymentsToChannel("something: " + CHANNEL_ID); - assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2)) - .isEqualTo(Coins.NONE); - } - - @Test - void getRebalanceTargetCostsForChannel_without_first_channel_information() { - Payment payment = new Payment( - PAYMENT_INDEX, PAYMENT_HASH, PAYMENT_CREATION_DATE_TIME, PAYMENT_VALUE, PAYMENT_FEES, List.of() - ); - SettledInvoice settledInvoice = new SettledInvoice( - ADD_INDEX, - SETTLE_INDEX, - SETTLE_DATE, - HASH, - AMOUNT_PAID, - "something: " + CHANNEL_ID, - Optional.empty(), - Optional.of(CHANNEL_ID_2) - ); - SelfPayment selfPayment = new SelfPayment(payment, settledInvoice); - when(selfPaymentsService.getSelfPaymentsToChannel(CHANNEL_ID_2)).thenReturn(List.of(selfPayment)); - assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2)) - .isEqualTo(PAYMENT_FEES); - } - @Test void getRebalanceTargetCostsForPeer() { - ChannelId id1 = LOCAL_OPEN_CHANNEL.getId(); - ChannelId id2 = CLOSED_CHANNEL_2.getId(); - when(channelService.getAllChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, CLOSED_CHANNEL_2)); - when(selfPaymentsService.getSelfPaymentsToChannel(id1)).thenReturn(List.of(getSelfPayment(""))); - when(selfPaymentsService.getSelfPaymentsToChannel(id2)).thenReturn(List.of(getSelfPayment(""))); + when(rebalanceService.getRebalancesToPeer(PUBKEY)).thenReturn(Set.of(SELF_PAYMENT, SELF_PAYMENT_2)); assertThat(offChainCostService.getRebalanceTargetCostsForPeer(PUBKEY)) .isEqualTo(COST_FOR_TWO_SELF_PAYMENTS); } - private void mockSelfPaymentsFromChannel(String memo) { - when(selfPaymentsService.getSelfPaymentsFromChannel(CHANNEL_ID)).thenReturn(List.of( - getSelfPayment(memo), - getSelfPayment(memo) - )); - } - - private void mockSelfPaymentsToChannel(String memo) { - when(selfPaymentsService.getSelfPaymentsToChannel(CHANNEL_ID_2)).thenReturn(List.of( - getSelfPayment(memo), - getSelfPayment(memo) - )); - } - - private SelfPayment getSelfPayment(String memo) { - List routes = getSingleRoute(); - Payment payment = new Payment( - PAYMENT_INDEX, PAYMENT_HASH, PAYMENT_CREATION_DATE_TIME, PAYMENT_VALUE, PAYMENT_FEES, routes - ); - SettledInvoice settledInvoice = new SettledInvoice( - ADD_INDEX, - SETTLE_INDEX, - SETTLE_DATE, - HASH, - AMOUNT_PAID, - memo, - Optional.empty(), - Optional.of(CHANNEL_ID_2) - ); - return new SelfPayment(payment, settledInvoice); - } - - private List getSingleRoute() { - PaymentHop firstHop = new PaymentHop(CHANNEL_ID, Coins.NONE); - PaymentHop lastHop = new PaymentHop(CHANNEL_ID_2, Coins.NONE); - PaymentRoute route = new PaymentRoute(List.of(firstHop, lastHop)); - return List.of(route); - } } \ No newline at end of file diff --git a/backend/src/test/java/de/cotto/lndmanagej/service/RebalanceServiceTest.java b/backend/src/test/java/de/cotto/lndmanagej/service/RebalanceServiceTest.java new file mode 100644 index 00000000..0109cef8 --- /dev/null +++ b/backend/src/test/java/de/cotto/lndmanagej/service/RebalanceServiceTest.java @@ -0,0 +1,200 @@ +package de.cotto.lndmanagej.service; + +import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.Payment; +import de.cotto.lndmanagej.model.PaymentHop; +import de.cotto.lndmanagej.model.PaymentRoute; +import de.cotto.lndmanagej.model.SelfPayment; +import de.cotto.lndmanagej.model.SettledInvoice; +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 java.util.Set; + +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.CoopClosedChannelFixtures.CLOSED_CHANNEL_2; +import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL; +import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_3; +import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_CREATION_DATE_TIME; +import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_FEES; +import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_HASH; +import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_INDEX; +import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_VALUE; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; +import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.ADD_INDEX; +import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.AMOUNT_PAID; +import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.HASH; +import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.SETTLE_DATE; +import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.SETTLE_INDEX; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RebalanceServiceTest { + @InjectMocks + private RebalanceService rebalanceService; + + @Mock + private SelfPaymentsService selfPaymentsService; + + @Mock + private ChannelService channelService; + + @Test + void getRebalancesFromPeer() { + ChannelId id1 = LOCAL_OPEN_CHANNEL.getId(); + ChannelId id2 = CLOSED_CHANNEL_2.getId(); + when(channelService.getAllChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, CLOSED_CHANNEL_2)); + SelfPayment selfPayment1 = getSelfPayment(id1.toString(), 0); + SelfPayment selfPayment2 = getSelfPayment(id2.toString(), 1); + when(selfPaymentsService.getSelfPaymentsFromChannel(id1)).thenReturn(List.of(selfPayment1)); + when(selfPaymentsService.getSelfPaymentsFromChannel(id2)).thenReturn(List.of(selfPayment2)); + assertThat(rebalanceService.getRebalancesFromPeer(PUBKEY)) + .containsExactlyInAnyOrder(selfPayment1, selfPayment2); + } + + @Test + void getRebalancesToPeer() { + ChannelId id1 = LOCAL_OPEN_CHANNEL_3.getId(); + ChannelId id2 = CLOSED_CHANNEL_2.getId(); + when(channelService.getAllChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL_3, CLOSED_CHANNEL_2)); + SelfPayment selfPayment1 = getSelfPayment(id1.toString(), 0); + SelfPayment selfPayment2 = getSelfPayment(id2.toString(), 1); + when(selfPaymentsService.getSelfPaymentsToChannel(id1)).thenReturn(List.of(selfPayment1)); + when(selfPaymentsService.getSelfPaymentsToChannel(id2)).thenReturn(List.of(selfPayment2)); + assertThat(rebalanceService.getRebalancesToPeer(PUBKEY)) + .containsExactlyInAnyOrder(selfPayment1, selfPayment2); + } + + @Test + void getRebalancesFromChannel_short_channel_id_in_memo() { + mockSelfPaymentsFromChannel("rebalance from " + CHANNEL_ID.getShortChannelId()); + assertThat(rebalanceService.getRebalancesFromChannel(CHANNEL_ID)).hasSize(2); + } + + @Test + void getRebalanceSourceCostsForChannel_compact_channel_id_in_memo() { + mockSelfPaymentsFromChannel("rebalance from " + CHANNEL_ID.getCompactForm()); + assertThat(rebalanceService.getRebalancesFromChannel(CHANNEL_ID)).hasSize(2); + } + + @Test + void getRebalanceSourceCostsForChannel_compact_lnd_channel_id_in_memo() { + mockSelfPaymentsFromChannel("rebalance from " + CHANNEL_ID.getCompactFormLnd()); + assertThat(rebalanceService.getRebalancesFromChannel(CHANNEL_ID)).hasSize(2); + } + + @Test + void getRebalanceSourceCostsForChannel_id_not_in_memo() { + mockSelfPaymentsFromChannel("something"); + assertThat(rebalanceService.getRebalancesFromChannel(CHANNEL_ID)).isEmpty(); + } + + @Test + void getRebalancesToChannel_no_self_payments() { + assertThat(rebalanceService.getRebalancesToChannel(CHANNEL_ID_2)).isEmpty(); + } + + @Test + void getRebalancesToChannel_short_channel_id_in_memo() { + mockSelfPaymentsToChannel("foo bar " + CHANNEL_ID_2.getShortChannelId() + "!"); + assertThat(rebalanceService.getRebalancesToChannel(CHANNEL_ID_2)).hasSize(2); + } + + @Test + void getRebalancesToChannel_compact_channel_id_in_memo() { + mockSelfPaymentsToChannel("something something " + CHANNEL_ID_2.getCompactForm()); + assertThat(rebalanceService.getRebalancesToChannel(CHANNEL_ID_2)).hasSize(2); + } + + @Test + void getRebalancesToChannel_compact_lnd_channel_id_in_memo() { + mockSelfPaymentsToChannel(CHANNEL_ID_2.getCompactFormLnd()); + assertThat(rebalanceService.getRebalancesToChannel(CHANNEL_ID_2)).hasSize(2); + } + + @Test + void getRebalancesToChannel_id_not_in_memo() { + mockSelfPaymentsToChannel("something"); + assertThat(rebalanceService.getRebalancesToChannel(CHANNEL_ID_2)).hasSize(2); + } + + @Test + void getRebalancesToChannel_other_channel_id_in_memo() { + mockSelfPaymentsToChannel("rebalance to " + CHANNEL_ID_3); + assertThat(rebalanceService.getRebalancesToChannel(CHANNEL_ID_2)).hasSize(2); + } + + @Test + void getRebalancesToChannel_source_channel_id_in_memo() { + mockSelfPaymentsToChannel("something: " + CHANNEL_ID); + assertThat(rebalanceService.getRebalancesToChannel(CHANNEL_ID_2)).isEmpty(); + } + + @Test + void getRebalancesToChannel_without_first_channel_information() { + Payment payment = new Payment( + PAYMENT_INDEX, PAYMENT_HASH, PAYMENT_CREATION_DATE_TIME, PAYMENT_VALUE, PAYMENT_FEES, List.of() + ); + SettledInvoice settledInvoice = new SettledInvoice( + ADD_INDEX, + SETTLE_INDEX, + SETTLE_DATE, + HASH, + AMOUNT_PAID, + "something: " + CHANNEL_ID, + Optional.empty(), + Optional.of(CHANNEL_ID_2) + ); + SelfPayment selfPayment = new SelfPayment(payment, settledInvoice); + when(selfPaymentsService.getSelfPaymentsToChannel(CHANNEL_ID_2)).thenReturn(List.of(selfPayment)); + assertThat(rebalanceService.getRebalancesToChannel(CHANNEL_ID_2)).containsExactly(selfPayment); + } + + private void mockSelfPaymentsFromChannel(String memo) { + when(selfPaymentsService.getSelfPaymentsFromChannel(CHANNEL_ID)).thenReturn(List.of( + getSelfPayment(memo, 0), + getSelfPayment(memo, 1) + )); + } + + private void mockSelfPaymentsToChannel(String memo) { + when(selfPaymentsService.getSelfPaymentsToChannel(CHANNEL_ID_2)).thenReturn(List.of( + getSelfPayment(memo, 0), + getSelfPayment(memo, 1) + )); + } + + private SelfPayment getSelfPayment(String memo, int offset) { + List routes = getSingleRoute(); + Payment payment = new Payment( + PAYMENT_INDEX, PAYMENT_HASH, PAYMENT_CREATION_DATE_TIME, PAYMENT_VALUE, PAYMENT_FEES, routes + ); + SettledInvoice settledInvoice = new SettledInvoice( + ADD_INDEX, + SETTLE_INDEX, + SETTLE_DATE.plusSeconds(offset), + HASH, + AMOUNT_PAID, + memo, + Optional.empty(), + Optional.of(CHANNEL_ID_2) + ); + return new SelfPayment(payment, settledInvoice); + } + + private List getSingleRoute() { + PaymentHop firstHop = new PaymentHop(CHANNEL_ID, Coins.NONE); + PaymentHop lastHop = new PaymentHop(CHANNEL_ID_2, Coins.NONE); + PaymentRoute route = new PaymentRoute(List.of(firstHop, lastHop)); + return List.of(route); + } +} \ No newline at end of file diff --git a/model/src/main/java/de/cotto/lndmanagej/model/OffChainCosts.java b/model/src/main/java/de/cotto/lndmanagej/model/OffChainCosts.java new file mode 100644 index 00000000..db408f9e --- /dev/null +++ b/model/src/main/java/de/cotto/lndmanagej/model/OffChainCosts.java @@ -0,0 +1,15 @@ +package de.cotto.lndmanagej.model; + +public record OffChainCosts( + Coins rebalanceSource, + Coins rebalanceTarget +) { + public static final OffChainCosts NONE = new OffChainCosts(Coins.NONE, Coins.NONE); + + public OffChainCosts add(OffChainCosts other) { + return new OffChainCosts( + rebalanceSource.add(other.rebalanceSource), + rebalanceTarget.add(other.rebalanceTarget) + ); + } +} diff --git a/model/src/test/java/de/cotto/lndmanagej/model/OffChainCostsTest.java b/model/src/test/java/de/cotto/lndmanagej/model/OffChainCostsTest.java new file mode 100644 index 00000000..0a6cefe5 --- /dev/null +++ b/model/src/test/java/de/cotto/lndmanagej/model/OffChainCostsTest.java @@ -0,0 +1,36 @@ +package de.cotto.lndmanagej.model; + +import org.junit.jupiter.api.Test; + +import static de.cotto.lndmanagej.model.OffChainCostsFixtures.OFF_CHAIN_COSTS; +import static org.assertj.core.api.Assertions.assertThat; + +class OffChainCostsTest { + @Test + void none() { + assertThat(OffChainCosts.NONE).isEqualTo(new OffChainCosts(Coins.NONE, Coins.NONE)); + } + + @Test + void add_none_to_none() { + assertThat(OffChainCosts.NONE.add(OffChainCosts.NONE)).isEqualTo(OffChainCosts.NONE); + } + + @Test + void add() { + assertThat(OFF_CHAIN_COSTS.add(OFF_CHAIN_COSTS)).isEqualTo(new OffChainCosts( + Coins.ofSatoshis(2000), + Coins.ofSatoshis(4000) + )); + } + + @Test + void rebalanceSourceCosts() { + assertThat(OFF_CHAIN_COSTS.rebalanceSource()).isEqualTo(Coins.ofSatoshis(1000)); + } + + @Test + void rebalanceTargetCosts() { + assertThat(OFF_CHAIN_COSTS.rebalanceTarget()).isEqualTo(Coins.ofSatoshis(2000)); + } +} \ No newline at end of file diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/OffChainCostsFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/OffChainCostsFixtures.java new file mode 100644 index 00000000..7dc4a209 --- /dev/null +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/OffChainCostsFixtures.java @@ -0,0 +1,8 @@ +package de.cotto.lndmanagej.model; + +public class OffChainCostsFixtures { + public static final OffChainCosts OFF_CHAIN_COSTS = new OffChainCosts( + Coins.ofSatoshis(1000), + Coins.ofSatoshis(2000) + ); +} diff --git a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java index 323053fa..4922a13e 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java @@ -30,6 +30,7 @@ import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHAN import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_2; import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_3; import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS_2; +import static de.cotto.lndmanagej.model.OffChainCostsFixtures.OFF_CHAIN_COSTS; import static de.cotto.lndmanagej.model.OnChainCostsFixtures.ON_CHAIN_COSTS; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; import static de.cotto.lndmanagej.model.WaitingCloseChannelFixtures.WAITING_CLOSE_CHANNEL; @@ -84,8 +85,7 @@ class NodeControllerIT { when(channelService.getWaitingCloseChannelsWith(PUBKEY_2)).thenReturn(Set.of(WAITING_CLOSE_CHANNEL)); when(channelService.getForceClosingChannelsWith(PUBKEY_2)).thenReturn(Set.of(FORCE_CLOSING_CHANNEL_2)); when(onChainCostService.getOnChainCostsForPeer(PUBKEY_2)).thenReturn(ON_CHAIN_COSTS); - when(offChainCostService.getRebalanceSourceCostsForPeer(PUBKEY_2)).thenReturn(Coins.ofMilliSatoshis(1)); - when(offChainCostService.getRebalanceTargetCostsForPeer(PUBKEY_2)).thenReturn(Coins.ofMilliSatoshis(2)); + when(offChainCostService.getOffChainCostsForPeer(PUBKEY_2)).thenReturn(OFF_CHAIN_COSTS); when(balanceService.getBalanceInformationForPeer(PUBKEY_2)).thenReturn(BALANCE_INFORMATION); when(feeService.getFeeReportForPeer(PUBKEY_2)).thenReturn(FEE_REPORT); List channelIds = List.of(CHANNEL_ID.toString(), CHANNEL_ID_2.toString()); @@ -99,8 +99,8 @@ class NodeControllerIT { .andExpect(jsonPath("$.closedChannels", is(closedChannelIds))) .andExpect(jsonPath("$.waitingCloseChannels", is(waitingCloseChannelIds))) .andExpect(jsonPath("$.pendingForceClosingChannels", is(forceClosingChannelIds))) - .andExpect(jsonPath("$.offChainCosts.rebalanceSource", is("1"))) - .andExpect(jsonPath("$.offChainCosts.rebalanceTarget", is("2"))) + .andExpect(jsonPath("$.offChainCosts.rebalanceSource", is("1000000"))) + .andExpect(jsonPath("$.offChainCosts.rebalanceTarget", is("2000000"))) .andExpect(jsonPath("$.balance.localBalance", is("1000"))) .andExpect(jsonPath("$.balance.localReserve", is("100"))) .andExpect(jsonPath("$.balance.localAvailable", is("900"))) diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/NodeController.java b/web/src/main/java/de/cotto/lndmanagej/controller/NodeController.java index 8ea83a63..9193e995 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/NodeController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/NodeController.java @@ -11,8 +11,8 @@ import de.cotto.lndmanagej.controller.dto.OnChainCostsDto; import de.cotto.lndmanagej.model.BalanceInformation; import de.cotto.lndmanagej.model.Channel; import de.cotto.lndmanagej.model.ChannelId; -import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.Node; +import de.cotto.lndmanagej.model.OffChainCosts; import de.cotto.lndmanagej.model.OnChainCosts; import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.service.BalanceService; @@ -68,8 +68,7 @@ public class NodeController { public NodeDetailsDto getDetails(@PathVariable Pubkey pubkey) { Node node = nodeService.getNode(pubkey); OnChainCosts onChainCosts = onChainCostService.getOnChainCostsForPeer(pubkey); - Coins rebalanceSourceCosts = offChainCostService.getRebalanceSourceCostsForPeer(pubkey); - Coins rebalanceTargetCosts = offChainCostService.getRebalanceTargetCostsForPeer(pubkey); + OffChainCosts offChainCosts = offChainCostService.getOffChainCostsForPeer(pubkey); BalanceInformation balanceInformation = balanceService.getBalanceInformationForPeer(pubkey); return new NodeDetailsDto( pubkey, @@ -79,7 +78,7 @@ public class NodeController { toSortedList(channelService.getWaitingCloseChannelsWith(pubkey)), toSortedList(channelService.getForceClosingChannelsWith(pubkey)), OnChainCostsDto.createFromModel(onChainCosts), - new OffChainCostsDto(rebalanceSourceCosts, rebalanceTargetCosts), + OffChainCostsDto.createFromModel(offChainCosts), BalanceInformationDto.createFromModel(balanceInformation), node.online(), getFeeReportDto(pubkey) diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/dto/OffChainCostsDto.java b/web/src/main/java/de/cotto/lndmanagej/controller/dto/OffChainCostsDto.java index 939f05c4..28e2b749 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/dto/OffChainCostsDto.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/dto/OffChainCostsDto.java @@ -1,6 +1,7 @@ package de.cotto.lndmanagej.controller.dto; import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.OffChainCosts; public record OffChainCostsDto(String rebalanceSource, String rebalanceTarget) { public OffChainCostsDto(Coins rebalanceSource, Coins rebalanceTarget) { @@ -9,4 +10,9 @@ public record OffChainCostsDto(String rebalanceSource, String rebalanceTarget) { String.valueOf(rebalanceTarget.milliSatoshis()) ); } + + public static OffChainCostsDto createFromModel(OffChainCosts offChainCosts) { + return new OffChainCostsDto(offChainCosts.rebalanceSource(), offChainCosts.rebalanceTarget()); + } + } diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java index 88cb9bfb..af7ec2ba 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java @@ -10,6 +10,7 @@ import de.cotto.lndmanagej.model.BalanceInformation; import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.FeeReport; import de.cotto.lndmanagej.model.Node; +import de.cotto.lndmanagej.model.OffChainCosts; import de.cotto.lndmanagej.model.OnChainCosts; import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.service.BalanceService; @@ -40,6 +41,7 @@ import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHAN import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_2; import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_3; import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS_2; +import static de.cotto.lndmanagej.model.OffChainCostsFixtures.OFF_CHAIN_COSTS; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; import static de.cotto.lndmanagej.model.WaitingCloseChannelFixtures.WAITING_CLOSE_CHANNEL; @@ -83,8 +85,7 @@ class NodeControllerTest { @Test void getNodeDetails_no_channels() { when(onChainCostService.getOnChainCostsForPeer(any())).thenReturn(OnChainCosts.NONE); - when(offChainCostService.getRebalanceSourceCostsForPeer(any())).thenReturn(Coins.NONE); - when(offChainCostService.getRebalanceTargetCostsForPeer(any())).thenReturn(Coins.NONE); + when(offChainCostService.getOffChainCostsForPeer(any())).thenReturn(OffChainCosts.NONE); when(balanceService.getBalanceInformationForPeer(any(Pubkey.class))).thenReturn(BalanceInformation.EMPTY); when(feeService.getFeeReportForPeer(any())).thenReturn(new FeeReport(Coins.NONE, Coins.NONE)); NodeDetailsDto expectedDetails = new NodeDetailsDto( @@ -95,7 +96,7 @@ class NodeControllerTest { List.of(), List.of(), OnChainCostsDto.createFromModel(OnChainCosts.NONE), - new OffChainCostsDto(Coins.NONE, Coins.NONE), + OffChainCostsDto.createFromModel(OffChainCosts.NONE), BalanceInformationDto.createFromModel(BalanceInformation.EMPTY), true, new FeeReportDto("0", "0") @@ -121,11 +122,8 @@ class NodeControllerTest { Coins.ofSatoshis(456), Coins.ofSatoshis(789) ); - Coins rebalanceSourceCosts = Coins.ofMilliSatoshis(1); - Coins rebalanceTargetCosts = Coins.ofMilliSatoshis(2); when(onChainCostService.getOnChainCostsForPeer(PUBKEY_2)).thenReturn(onChainCosts); - when(offChainCostService.getRebalanceSourceCostsForPeer(PUBKEY_2)).thenReturn(rebalanceSourceCosts); - when(offChainCostService.getRebalanceTargetCostsForPeer(PUBKEY_2)).thenReturn(rebalanceTargetCosts); + when(offChainCostService.getOffChainCostsForPeer(PUBKEY_2)).thenReturn(OFF_CHAIN_COSTS); when(balanceService.getBalanceInformationForPeer(PUBKEY_2)).thenReturn(BALANCE_INFORMATION); when(feeService.getFeeReportForPeer(PUBKEY_2)).thenReturn(FEE_REPORT); NodeDetailsDto expectedDetails = new NodeDetailsDto( @@ -136,7 +134,7 @@ class NodeControllerTest { List.of(CHANNEL_ID, CHANNEL_ID_2), List.of(CHANNEL_ID, CHANNEL_ID_2, CHANNEL_ID_3), OnChainCostsDto.createFromModel(onChainCosts), - new OffChainCostsDto(rebalanceSourceCosts, rebalanceTargetCosts), + OffChainCostsDto.createFromModel(OFF_CHAIN_COSTS), BalanceInformationDto.createFromModel(BALANCE_INFORMATION), false, new FeeReportDto("1234", "567") diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/dto/OffChainCostsDtoTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/dto/OffChainCostsDtoTest.java index dca95f6b..d4ce63ed 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/dto/OffChainCostsDtoTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/dto/OffChainCostsDtoTest.java @@ -3,6 +3,7 @@ package de.cotto.lndmanagej.controller.dto; import de.cotto.lndmanagej.model.Coins; import org.junit.jupiter.api.Test; +import static de.cotto.lndmanagej.model.OffChainCostsFixtures.OFF_CHAIN_COSTS; import static org.assertj.core.api.Assertions.assertThat; class OffChainCostsDtoTest { @@ -12,4 +13,10 @@ class OffChainCostsDtoTest { assertThat(dto.rebalanceSource()).isEqualTo("1"); assertThat(dto.rebalanceTarget()).isEqualTo("1234"); } + + @Test + void createFromModel() { + assertThat(OffChainCostsDto.createFromModel(OFF_CHAIN_COSTS)) + .isEqualTo(new OffChainCostsDto("1000000", "2000000")); + } } \ No newline at end of file