diff --git a/backend/src/main/java/de/cotto/lndmanagej/service/FeeService.java b/backend/src/main/java/de/cotto/lndmanagej/service/FeeService.java index 224a3ffc..d2332b0f 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/FeeService.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/FeeService.java @@ -24,10 +24,23 @@ public class FeeService { .reduce(Coins.NONE, Coins::add); } + public Coins getSourcedFeesForChannel(ChannelId channelId) { + return forwardingEventsDao.getEventsWithIncomingChannel(channelId).parallelStream() + .map(ForwardingEvent::fees) + .reduce(Coins.NONE, Coins::add); + } + public Coins getEarnedFeesForPeer(Pubkey peer) { return channelService.getAllChannelsWith(peer).parallelStream() .map(Channel::getId) .map(this::getEarnedFeesForChannel) .reduce(Coins.NONE, Coins::add); } + + public Coins getSourcedFeesForPeer(Pubkey peer) { + return channelService.getAllChannelsWith(peer).parallelStream() + .map(Channel::getId) + .map(this::getSourcedFeesForChannel) + .reduce(Coins.NONE, Coins::add); + } } diff --git a/backend/src/test/java/de/cotto/lndmanagej/service/FeeServiceTest.java b/backend/src/test/java/de/cotto/lndmanagej/service/FeeServiceTest.java index 90b8acc6..f4f62018 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/FeeServiceTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/FeeServiceTest.java @@ -49,6 +49,22 @@ class FeeServiceTest { assertThat(feeService.getEarnedFeesForPeer(PUBKEY)).isEqualTo(Coins.NONE); } + @Test + void getSourcedFeesForChannel() { + when(dao.getEventsWithIncomingChannel(CHANNEL_ID)).thenReturn(List.of(FORWARDING_EVENT, FORWARDING_EVENT_2)); + assertThat(feeService.getSourcedFeesForChannel(CHANNEL_ID)).isEqualTo(Coins.ofMilliSatoshis(101)); + } + + @Test + void getSourcedFeesForChannel_no_forward() { + assertThat(feeService.getSourcedFeesForChannel(CHANNEL_ID)).isEqualTo(Coins.NONE); + } + + @Test + void getSourcedFeesForPeer_no_channel() { + assertThat(feeService.getSourcedFeesForPeer(PUBKEY)).isEqualTo(Coins.NONE); + } + @Test void getEarnedFeesForPeer() { when(dao.getEventsWithOutgoingChannel(CLOSED_CHANNEL.getId())).thenReturn(List.of(FORWARDING_EVENT_3)); @@ -58,4 +74,14 @@ class FeeServiceTest { .thenReturn(Set.of(CLOSED_CHANNEL, WAITING_CLOSE_CHANNEL_2, LOCAL_OPEN_CHANNEL_3)); assertThat(feeService.getEarnedFeesForPeer(PUBKEY)).isEqualTo(Coins.ofMilliSatoshis(5_101)); } + + @Test + void getSourcedFeesForPeer() { + when(dao.getEventsWithIncomingChannel(CLOSED_CHANNEL.getId())).thenReturn(List.of(FORWARDING_EVENT_3)); + when(dao.getEventsWithIncomingChannel(WAITING_CLOSE_CHANNEL_2.getId())).thenReturn(List.of(FORWARDING_EVENT)); + when(dao.getEventsWithIncomingChannel(LOCAL_OPEN_CHANNEL_3.getId())).thenReturn(List.of(FORWARDING_EVENT_2)); + when(channelService.getAllChannelsWith(PUBKEY)) + .thenReturn(Set.of(CLOSED_CHANNEL, WAITING_CLOSE_CHANNEL_2, LOCAL_OPEN_CHANNEL_3)); + assertThat(feeService.getSourcedFeesForPeer(PUBKEY)).isEqualTo(Coins.ofMilliSatoshis(5_101)); + } } \ No newline at end of file diff --git a/forwarding-history/src/main/java/de/cotto/lndmanagej/statistics/ForwardingEventsDao.java b/forwarding-history/src/main/java/de/cotto/lndmanagej/statistics/ForwardingEventsDao.java index bc0c304f..14b6c227 100644 --- a/forwarding-history/src/main/java/de/cotto/lndmanagej/statistics/ForwardingEventsDao.java +++ b/forwarding-history/src/main/java/de/cotto/lndmanagej/statistics/ForwardingEventsDao.java @@ -12,4 +12,6 @@ public interface ForwardingEventsDao { int getOffset(); List getEventsWithOutgoingChannel(ChannelId channelId); + + List getEventsWithIncomingChannel(ChannelId channelId); } diff --git a/forwarding-history/src/main/java/de/cotto/lndmanagej/statistics/persistence/ForwardingEventsDaoImpl.java b/forwarding-history/src/main/java/de/cotto/lndmanagej/statistics/persistence/ForwardingEventsDaoImpl.java index 5ad1ecb3..db363a17 100644 --- a/forwarding-history/src/main/java/de/cotto/lndmanagej/statistics/persistence/ForwardingEventsDaoImpl.java +++ b/forwarding-history/src/main/java/de/cotto/lndmanagej/statistics/persistence/ForwardingEventsDaoImpl.java @@ -39,4 +39,11 @@ public class ForwardingEventsDaoImpl implements ForwardingEventsDao { .map(ForwardingEventJpaDto::toModel) .toList(); } + + @Override + public List getEventsWithIncomingChannel(ChannelId channelId) { + return repository.findByChannelIncoming(channelId.getShortChannelId()).stream() + .map(ForwardingEventJpaDto::toModel) + .toList(); + } } diff --git a/forwarding-history/src/test/java/de/cotto/lndmanagej/statistics/persistence/ForwardingEventsDaoImplTest.java b/forwarding-history/src/test/java/de/cotto/lndmanagej/statistics/persistence/ForwardingEventsDaoImplTest.java index 64da9191..cef52a2d 100644 --- a/forwarding-history/src/test/java/de/cotto/lndmanagej/statistics/persistence/ForwardingEventsDaoImplTest.java +++ b/forwarding-history/src/test/java/de/cotto/lndmanagej/statistics/persistence/ForwardingEventsDaoImplTest.java @@ -71,6 +71,21 @@ class ForwardingEventsDaoImplTest { .containsExactly(FORWARDING_EVENT, FORWARDING_EVENT_2); } + @Test + void getEventsWithIncomingChannel_empty() { + assertThat(dao.getEventsWithIncomingChannel(CHANNEL_ID)).isEmpty(); + } + + @Test + void getEventsWithIncomingChannel() { + when(repository.findByChannelIncoming(CHANNEL_ID_2.getShortChannelId())).thenReturn(List.of( + ForwardingEventJpaDto.createFromForwardingEvent(FORWARDING_EVENT), + ForwardingEventJpaDto.createFromForwardingEvent(FORWARDING_EVENT_2) + )); + assertThat(dao.getEventsWithIncomingChannel(CHANNEL_ID_2)) + .containsExactly(FORWARDING_EVENT, FORWARDING_EVENT_2); + } + @SuppressWarnings("PMD.LinguisticNaming") private ArgumentMatcher> isSet(Set expected) { return iterable -> iterable instanceof List && ((List) iterable).stream() diff --git a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelControllerIT.java b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelControllerIT.java index a234f1af..8c8f8485 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelControllerIT.java @@ -127,6 +127,7 @@ class ChannelControllerIT { when(onChainCostService.getCloseCosts(CHANNEL_ID)).thenReturn(Optional.of(Coins.ofSatoshis(2000))); when(balanceService.getBalanceInformation(CHANNEL_ID)).thenReturn(Optional.of(BALANCE_INFORMATION_2)); when(feeService.getEarnedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(1_234)); + when(feeService.getSourcedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(567)); mockMvc.perform(get(DETAILS_PREFIX)) .andExpect(jsonPath("$.channelIdShort", is(String.valueOf(CHANNEL_ID.getShortChannelId())))) .andExpect(jsonPath("$.channelIdCompact", is(CHANNEL_ID.getCompactForm()))) @@ -157,13 +158,15 @@ class ChannelControllerIT { .andExpect(jsonPath("$.policies.local.baseFeeMilliSat", is(10))) .andExpect(jsonPath("$.policies.remote.feeRatePpm", is(222))) .andExpect(jsonPath("$.policies.remote.baseFeeMilliSat", is(0))) - .andExpect(jsonPath("$.feeReport.earned", is("1234"))); + .andExpect(jsonPath("$.feeReport.earned", is("1234"))) + .andExpect(jsonPath("$.feeReport.sourced", is("567"))); } @Test void getChannelDetails_closed_channel() throws Exception { when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(CLOSED_CHANNEL)); when(feeService.getEarnedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.NONE); + when(feeService.getSourcedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.NONE); mockMvc.perform(get(DETAILS_PREFIX)) .andExpect(jsonPath("$.closeDetails.initiator", is("REMOTE"))) .andExpect(jsonPath("$.closeDetails.height", is(987_654))) @@ -225,7 +228,9 @@ class ChannelControllerIT { @Test void getFeeReport() throws Exception { when(feeService.getEarnedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(1_234)); + when(feeService.getSourcedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(567)); mockMvc.perform(get(CHANNEL_PREFIX + "/fee-report")) - .andExpect(jsonPath("$.earned", is("1234"))); + .andExpect(jsonPath("$.earned", is("1234"))) + .andExpect(jsonPath("$.sourced", is("567"))); } } \ No newline at end of file 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 1893cfad..2d513277 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java @@ -85,6 +85,7 @@ class NodeControllerIT { when(onChainCostService.getCloseCostsWith(PUBKEY_2)).thenReturn(Coins.ofSatoshis(456)); when(balanceService.getBalanceInformation(PUBKEY_2)).thenReturn(BALANCE_INFORMATION); when(feeService.getEarnedFeesForPeer(PUBKEY_2)).thenReturn(Coins.ofMilliSatoshis(1_234)); + when(feeService.getSourcedFeesForPeer(PUBKEY_2)).thenReturn(Coins.ofMilliSatoshis(567)); List channelIds = List.of(CHANNEL_ID.toString(), CHANNEL_ID_2.toString()); List closedChannelIds = List.of(CHANNEL_ID.toString(), CHANNEL_ID_3.toString()); List waitingCloseChannelIds = List.of(CHANNEL_ID.toString()); @@ -105,6 +106,7 @@ class NodeControllerIT { .andExpect(jsonPath("$.balance.remoteReserve", is("10"))) .andExpect(jsonPath("$.balance.remoteAvailable", is("113"))) .andExpect(jsonPath("$.feeReport.earned", is("1234"))) + .andExpect(jsonPath("$.feeReport.sourced", is("567"))) .andExpect(jsonPath("$.online", is(true))); } @@ -141,7 +143,9 @@ class NodeControllerIT { @Test void getFeeReport() throws Exception { when(feeService.getEarnedFeesForPeer(PUBKEY_2)).thenReturn(Coins.ofMilliSatoshis(1_234)); + when(feeService.getSourcedFeesForPeer(PUBKEY_2)).thenReturn(Coins.ofMilliSatoshis(567)); mockMvc.perform(get(NODE_PREFIX + "/fee-report")) - .andExpect(jsonPath("$.earned", is("1234"))); + .andExpect(jsonPath("$.earned", is("1234"))) + .andExpect(jsonPath("$.sourced", is("567"))); } } \ No newline at end of file diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/ChannelController.java b/web/src/main/java/de/cotto/lndmanagej/controller/ChannelController.java index 2a5e89e3..f96abba4 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/ChannelController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/ChannelController.java @@ -127,7 +127,8 @@ public class ChannelController { private FeeReportDto getFeeReportDto(ChannelId channelId) { Coins earned = feeService.getEarnedFeesForChannel(channelId); - return new FeeReportDto(earned); + Coins sourced = feeService.getSourcedFeesForChannel(channelId); + return new FeeReportDto(earned, sourced); } private PoliciesDto getPoliciesForChannel(@Nullable LocalChannel channel) { 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 634fbc55..bfd1e2fd 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/NodeController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/NodeController.java @@ -110,7 +110,7 @@ public class NodeController { } private FeeReportDto getFeeReportDto(Pubkey pubkey) { - return new FeeReportDto(feeService.getEarnedFeesForPeer(pubkey)); + return new FeeReportDto(feeService.getEarnedFeesForPeer(pubkey), feeService.getSourcedFeesForPeer(pubkey)); } private List toSortedList(Set channels) { diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/dto/FeeReportDto.java b/web/src/main/java/de/cotto/lndmanagej/controller/dto/FeeReportDto.java index c2bce112..11feb446 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/dto/FeeReportDto.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/dto/FeeReportDto.java @@ -2,8 +2,11 @@ package de.cotto.lndmanagej.controller.dto; import de.cotto.lndmanagej.model.Coins; -public record FeeReportDto(String earned) { - public FeeReportDto(Coins earned) { - this(String.valueOf(earned.milliSatoshis())); +public record FeeReportDto(String earned, String sourced) { + public FeeReportDto(Coins earned, Coins sourced) { + this( + String.valueOf(earned.milliSatoshis()), + String.valueOf(sourced.milliSatoshis()) + ); } } diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/ChannelControllerTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/ChannelControllerTest.java index fb17433f..00ecf25d 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/ChannelControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/ChannelControllerTest.java @@ -49,7 +49,8 @@ class ChannelControllerTest { private static final Coins CLOSE_COSTS = Coins.ofSatoshis(2); private static final OnChainCostsDto ON_CHAIN_COSTS = new OnChainCostsDto(OPEN_COSTS, CLOSE_COSTS); private static final PoliciesDto FEE_CONFIGURATION_DTO = PoliciesDto.createFrom(POLICIES); - private static final FeeReportDto FEE_REPORT_DTO = new FeeReportDto(Coins.ofMilliSatoshis(1234)); + private static final FeeReportDto FEE_REPORT_DTO = + new FeeReportDto(Coins.ofMilliSatoshis(1234), Coins.ofMilliSatoshis(567)); private static final ClosedChannelDetailsDto CLOSED_CHANNEL_DETAILS_DTO = new ClosedChannelDetailsDto(CloseInitiator.REMOTE, 987_654); @@ -83,6 +84,7 @@ class ChannelControllerTest { lenient().when(onChainCostService.getCloseCosts(CHANNEL_ID)).thenReturn(Optional.of(CLOSE_COSTS)); lenient().when(policyService.getPolicies(CHANNEL_ID)).thenReturn(POLICIES); lenient().when(feeService.getEarnedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(1_234)); + lenient().when(feeService.getSourcedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(567)); } @Test @@ -212,8 +214,9 @@ class ChannelControllerTest { @Test void getFeeReport() { - when(feeService.getEarnedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofSatoshis(123)); - assertThat(channelController.getFeeReport(CHANNEL_ID)).isEqualTo(new FeeReportDto(Coins.ofSatoshis(123))); + when(feeService.getEarnedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(1_234)); + when(feeService.getSourcedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(567)); + assertThat(channelController.getFeeReport(CHANNEL_ID)).isEqualTo(FEE_REPORT_DTO); verify(metrics).mark(argThat(name -> name.endsWith(".getFeeReport"))); } 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 52976aa8..654d7005 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java @@ -84,6 +84,7 @@ class NodeControllerTest { when(onChainCostService.getCloseCostsWith(any())).thenReturn(Coins.NONE); when(balanceService.getBalanceInformation(any(Pubkey.class))).thenReturn(BalanceInformation.EMPTY); when(feeService.getEarnedFeesForPeer(any())).thenReturn(Coins.NONE); + when(feeService.getSourcedFeesForPeer(any())).thenReturn(Coins.NONE); NodeDetailsDto expectedDetails = new NodeDetailsDto( PUBKEY_2, ALIAS_2, @@ -94,7 +95,7 @@ class NodeControllerTest { new OnChainCostsDto(Coins.NONE, Coins.NONE), BalanceInformationDto.createFrom(BalanceInformation.EMPTY), true, - new FeeReportDto("0") + new FeeReportDto("0", "0") ); when(nodeService.getNode(PUBKEY_2)).thenReturn(new Node(PUBKEY_2, ALIAS_2, 0, true)); @@ -119,6 +120,7 @@ class NodeControllerTest { when(onChainCostService.getCloseCostsWith(PUBKEY_2)).thenReturn(closeCosts); when(balanceService.getBalanceInformation(PUBKEY_2)).thenReturn(BALANCE_INFORMATION); when(feeService.getEarnedFeesForPeer(any())).thenReturn(Coins.ofMilliSatoshis(1234)); + when(feeService.getSourcedFeesForPeer(any())).thenReturn(Coins.ofMilliSatoshis(567)); NodeDetailsDto expectedDetails = new NodeDetailsDto( PUBKEY_2, ALIAS_2, @@ -129,7 +131,7 @@ class NodeControllerTest { new OnChainCostsDto(openCosts, closeCosts), BalanceInformationDto.createFrom(BALANCE_INFORMATION), false, - new FeeReportDto("1234") + new FeeReportDto("1234", "567") ); assertThat(nodeController.getDetails(PUBKEY_2)).isEqualTo(expectedDetails); @@ -175,7 +177,8 @@ class NodeControllerTest { @Test void getFeeReport() { when(feeService.getEarnedFeesForPeer(PUBKEY)).thenReturn(Coins.ofMilliSatoshis(1_234)); - assertThat(nodeController.getFeeReport(PUBKEY)).isEqualTo(new FeeReportDto("1234")); + when(feeService.getSourcedFeesForPeer(PUBKEY)).thenReturn(Coins.ofMilliSatoshis(567)); + assertThat(nodeController.getFeeReport(PUBKEY)).isEqualTo(new FeeReportDto("1234", "567")); verify(metrics).mark(argThat(name -> name.endsWith(".getFeeReport"))); } } \ No newline at end of file diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDtoTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDtoTest.java index 356881c2..bf0dfab2 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDtoTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDtoTest.java @@ -20,6 +20,8 @@ class ChannelDetailsDtoTest { private static final OnChainCostsDto ON_CHAIN_COSTS = new OnChainCostsDto(Coins.ofSatoshis(1), Coins.ofSatoshis(2)); private static final ClosedChannelDetailsDto CLOSE_DETAILS = new ClosedChannelDetailsDto("abc", 123); + private static final FeeReportDto FEE_REPORT = + new FeeReportDto(Coins.ofMilliSatoshis(1234), Coins.ofMilliSatoshis(567)); private static final ChannelDetailsDto CHANNEL_DETAILS_DTO = new ChannelDetailsDto( CLOSED_CHANNEL, ALIAS, @@ -27,7 +29,7 @@ class ChannelDetailsDtoTest { ON_CHAIN_COSTS, PoliciesDto.EMPTY, CLOSE_DETAILS, - new FeeReportDto(Coins.ofMilliSatoshis(1234)) + FEE_REPORT ); @Test @@ -84,7 +86,7 @@ class ChannelDetailsDtoTest { ON_CHAIN_COSTS, PoliciesDto.EMPTY, CLOSE_DETAILS, - new FeeReportDto(Coins.ofMilliSatoshis(1234)) + FEE_REPORT ); ChannelStatusDto channelStatusDto = ChannelStatusDto.createFrom(new ChannelStatus(false, true, false, OPEN)); @@ -103,7 +105,7 @@ class ChannelDetailsDtoTest { @Test void feeReport() { - assertThat(CHANNEL_DETAILS_DTO.feeReport()).isEqualTo(new FeeReportDto(Coins.ofMilliSatoshis(1_234))); + assertThat(CHANNEL_DETAILS_DTO.feeReport()).isEqualTo(FEE_REPORT); } @Test diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/dto/FeeReportDtoTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/dto/FeeReportDtoTest.java index 63ba95e6..12e2a4e5 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/dto/FeeReportDtoTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/dto/FeeReportDtoTest.java @@ -6,8 +6,17 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class FeeReportDtoTest { + + private static final FeeReportDto FEE_REPORT_DTO = + new FeeReportDto(Coins.ofMilliSatoshis(1_234), Coins.ofMilliSatoshis(567)); + @Test void earned() { - assertThat(new FeeReportDto(Coins.ofMilliSatoshis(1_234)).earned()).isEqualTo("1234"); + assertThat(FEE_REPORT_DTO.earned()).isEqualTo("1234"); + } + + @Test + void sourced() { + assertThat(FEE_REPORT_DTO.sourced()).isEqualTo("567"); } } \ No newline at end of file