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 0ff957d3..224a3ffc 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/FeeService.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/FeeService.java @@ -1,22 +1,33 @@ package de.cotto.lndmanagej.service; +import de.cotto.lndmanagej.model.Channel; import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.ForwardingEvent; +import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.statistics.ForwardingEventsDao; import org.springframework.stereotype.Component; @Component public class FeeService { private final ForwardingEventsDao forwardingEventsDao; + private final ChannelService channelService; - public FeeService(ForwardingEventsDao forwardingEventsDao) { + public FeeService(ForwardingEventsDao forwardingEventsDao, ChannelService channelService) { this.forwardingEventsDao = forwardingEventsDao; + this.channelService = channelService; } public Coins getEarnedFeesForChannel(ChannelId channelId) { - return forwardingEventsDao.getEventsWithOutgoingChannel(channelId).stream() + return forwardingEventsDao.getEventsWithOutgoingChannel(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); + } } 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 c09d90b3..90b8acc6 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/FeeServiceTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/FeeServiceTest.java @@ -9,10 +9,16 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.List; +import java.util.Set; import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL; import static de.cotto.lndmanagej.model.ForwardingEventFixtures.FORWARDING_EVENT; import static de.cotto.lndmanagej.model.ForwardingEventFixtures.FORWARDING_EVENT_2; +import static de.cotto.lndmanagej.model.ForwardingEventFixtures.FORWARDING_EVENT_3; +import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_3; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; +import static de.cotto.lndmanagej.model.WaitingCloseChannelFixtures.WAITING_CLOSE_CHANNEL_2; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -24,6 +30,9 @@ class FeeServiceTest { @Mock private ForwardingEventsDao dao; + @Mock + private ChannelService channelService; + @Test void getEarnedFeesForChannel() { when(dao.getEventsWithOutgoingChannel(CHANNEL_ID)).thenReturn(List.of(FORWARDING_EVENT, FORWARDING_EVENT_2)); @@ -34,4 +43,19 @@ class FeeServiceTest { void getEarnedFeesForChannel_no_forward() { assertThat(feeService.getEarnedFeesForChannel(CHANNEL_ID)).isEqualTo(Coins.NONE); } + + @Test + void getEarnedFeesForPeer_no_channel() { + assertThat(feeService.getEarnedFeesForPeer(PUBKEY)).isEqualTo(Coins.NONE); + } + + @Test + void getEarnedFeesForPeer() { + when(dao.getEventsWithOutgoingChannel(CLOSED_CHANNEL.getId())).thenReturn(List.of(FORWARDING_EVENT_3)); + when(dao.getEventsWithOutgoingChannel(WAITING_CLOSE_CHANNEL_2.getId())).thenReturn(List.of(FORWARDING_EVENT)); + when(dao.getEventsWithOutgoingChannel(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.getEarnedFeesForPeer(PUBKEY)).isEqualTo(Coins.ofMilliSatoshis(5_101)); + } } \ No newline at end of file diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardingEventFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardingEventFixtures.java index 360ad6b9..ff949343 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardingEventFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForwardingEventFixtures.java @@ -5,6 +5,7 @@ import java.time.LocalDateTime; 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.ChannelIdFixtures.CHANNEL_ID_4; public class ForwardingEventFixtures { public static final ForwardingEvent FORWARDING_EVENT = new ForwardingEvent( @@ -24,4 +25,13 @@ public class ForwardingEventFixtures { CHANNEL_ID, LocalDateTime.of(2021, 11, 29, 18, 30, 1, 500_000_000) ); + + public static final ForwardingEvent FORWARDING_EVENT_3 = new ForwardingEvent( + 3, + Coins.ofMilliSatoshis(30_000), + Coins.ofMilliSatoshis(25_000), + CHANNEL_ID_2, + CHANNEL_ID_4, + LocalDateTime.of(2021, 11, 29, 18, 30, 2) + ); } 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 a7ae1180..1893cfad 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java @@ -6,6 +6,7 @@ import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.Node; import de.cotto.lndmanagej.service.BalanceService; import de.cotto.lndmanagej.service.ChannelService; +import de.cotto.lndmanagej.service.FeeService; import de.cotto.lndmanagej.service.NodeService; import de.cotto.lndmanagej.service.OnChainCostService; import org.junit.jupiter.api.Test; @@ -59,6 +60,9 @@ class NodeControllerIT { @MockBean private BalanceService balanceService; + @MockBean + private FeeService feeService; + @MockBean @SuppressWarnings("unused") private Metrics metrics; @@ -80,6 +84,7 @@ class NodeControllerIT { when(onChainCostService.getOpenCostsWith(PUBKEY_2)).thenReturn(Coins.ofSatoshis(123)); 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)); 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()); @@ -99,6 +104,7 @@ class NodeControllerIT { .andExpect(jsonPath("$.balance.remoteBalance", is("123"))) .andExpect(jsonPath("$.balance.remoteReserve", is("10"))) .andExpect(jsonPath("$.balance.remoteAvailable", is("113"))) + .andExpect(jsonPath("$.feeReport.earned", is("1234"))) .andExpect(jsonPath("$.online", is(true))); } @@ -132,4 +138,10 @@ class NodeControllerIT { .andExpect(jsonPath("$.remoteAvailable", is("113"))); } + @Test + void getFeeReport() throws Exception { + when(feeService.getEarnedFeesForPeer(PUBKEY_2)).thenReturn(Coins.ofMilliSatoshis(1_234)); + mockMvc.perform(get(NODE_PREFIX + "/fee-report")) + .andExpect(jsonPath("$.earned", is("1234"))); + } } \ No newline at end of file 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 b48ef3e4..634fbc55 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/NodeController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/NodeController.java @@ -3,6 +3,7 @@ package de.cotto.lndmanagej.controller; import com.codahale.metrics.MetricRegistry; import de.cotto.lndmanagej.controller.dto.BalanceInformationDto; import de.cotto.lndmanagej.controller.dto.ChannelsForNodeDto; +import de.cotto.lndmanagej.controller.dto.FeeReportDto; import de.cotto.lndmanagej.controller.dto.NodeDetailsDto; import de.cotto.lndmanagej.controller.dto.ObjectMapperConfiguration; import de.cotto.lndmanagej.controller.dto.OnChainCostsDto; @@ -15,6 +16,7 @@ import de.cotto.lndmanagej.model.Node; import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.service.BalanceService; import de.cotto.lndmanagej.service.ChannelService; +import de.cotto.lndmanagej.service.FeeService; import de.cotto.lndmanagej.service.NodeService; import de.cotto.lndmanagej.service.OnChainCostService; import org.springframework.context.annotation.Import; @@ -36,23 +38,26 @@ public class NodeController { private final ChannelService channelService; private final OnChainCostService onChainCostService; private final BalanceService balanceService; + private final FeeService feeService; public NodeController( NodeService nodeService, ChannelService channelService, - Metrics metrics, OnChainCostService onChainCostService, - BalanceService balanceService + BalanceService balanceService, + FeeService feeService, + Metrics metrics ) { this.nodeService = nodeService; - this.metrics = metrics; this.channelService = channelService; this.onChainCostService = onChainCostService; this.balanceService = balanceService; + this.feeService = feeService; + this.metrics = metrics; } @GetMapping("/alias") - public String getAlias(Pubkey pubkey) { + public String getAlias(@PathVariable Pubkey pubkey) { mark("getAlias"); return nodeService.getAlias(pubkey); } @@ -73,7 +78,8 @@ public class NodeController { toSortedList(channelService.getForceClosingChannelsFor(pubkey)), new OnChainCostsDto(openCosts, closeCosts), BalanceInformationDto.createFrom(balanceInformation), - node.online() + node.online(), + getFeeReportDto(pubkey) ); } @@ -97,6 +103,16 @@ public class NodeController { return BalanceInformationDto.createFrom(balanceService.getBalanceInformation(pubkey)); } + @GetMapping("/fee-report") + public FeeReportDto getFeeReport(@PathVariable Pubkey pubkey) { + mark("getFeeReport"); + return getFeeReportDto(pubkey); + } + + private FeeReportDto getFeeReportDto(Pubkey pubkey) { + return new FeeReportDto(feeService.getEarnedFeesForPeer(pubkey)); + } + private List toSortedList(Set channels) { return channels.stream() .map(Channel::getId) diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/dto/NodeDetailsDto.java b/web/src/main/java/de/cotto/lndmanagej/controller/dto/NodeDetailsDto.java index 2cdb31a2..3874d50a 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/dto/NodeDetailsDto.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/dto/NodeDetailsDto.java @@ -16,6 +16,7 @@ public record NodeDetailsDto( List pendingForceClosingChannels, OnChainCostsDto onChainCosts, BalanceInformationDto balance, - boolean online + boolean online, + FeeReportDto feeReport ) { } 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 b10a594b..52976aa8 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java @@ -2,6 +2,7 @@ package de.cotto.lndmanagej.controller; import de.cotto.lndmanagej.controller.dto.BalanceInformationDto; import de.cotto.lndmanagej.controller.dto.ChannelsForNodeDto; +import de.cotto.lndmanagej.controller.dto.FeeReportDto; import de.cotto.lndmanagej.controller.dto.NodeDetailsDto; import de.cotto.lndmanagej.controller.dto.OnChainCostsDto; import de.cotto.lndmanagej.metrics.Metrics; @@ -11,6 +12,7 @@ import de.cotto.lndmanagej.model.Node; import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.service.BalanceService; import de.cotto.lndmanagej.service.ChannelService; +import de.cotto.lndmanagej.service.FeeService; import de.cotto.lndmanagej.service.NodeService; import de.cotto.lndmanagej.service.OnChainCostService; import org.junit.jupiter.api.Test; @@ -65,6 +67,9 @@ class NodeControllerTest { @Mock private BalanceService balanceService; + @Mock + private FeeService feeService; + @Test void getAlias() { when(nodeService.getAlias(PUBKEY_2)).thenReturn(ALIAS_2); @@ -78,6 +83,7 @@ class NodeControllerTest { when(onChainCostService.getOpenCostsWith(any())).thenReturn(Coins.NONE); when(onChainCostService.getCloseCostsWith(any())).thenReturn(Coins.NONE); when(balanceService.getBalanceInformation(any(Pubkey.class))).thenReturn(BalanceInformation.EMPTY); + when(feeService.getEarnedFeesForPeer(any())).thenReturn(Coins.NONE); NodeDetailsDto expectedDetails = new NodeDetailsDto( PUBKEY_2, ALIAS_2, @@ -87,7 +93,8 @@ class NodeControllerTest { List.of(), new OnChainCostsDto(Coins.NONE, Coins.NONE), BalanceInformationDto.createFrom(BalanceInformation.EMPTY), - true + true, + new FeeReportDto("0") ); when(nodeService.getNode(PUBKEY_2)).thenReturn(new Node(PUBKEY_2, ALIAS_2, 0, true)); @@ -111,6 +118,7 @@ class NodeControllerTest { when(onChainCostService.getOpenCostsWith(PUBKEY_2)).thenReturn(openCosts); when(onChainCostService.getCloseCostsWith(PUBKEY_2)).thenReturn(closeCosts); when(balanceService.getBalanceInformation(PUBKEY_2)).thenReturn(BALANCE_INFORMATION); + when(feeService.getEarnedFeesForPeer(any())).thenReturn(Coins.ofMilliSatoshis(1234)); NodeDetailsDto expectedDetails = new NodeDetailsDto( PUBKEY_2, ALIAS_2, @@ -120,7 +128,8 @@ class NodeControllerTest { List.of(CHANNEL_ID, CHANNEL_ID_2, CHANNEL_ID_3), new OnChainCostsDto(openCosts, closeCosts), BalanceInformationDto.createFrom(BALANCE_INFORMATION), - false + false, + new FeeReportDto("1234") ); assertThat(nodeController.getDetails(PUBKEY_2)).isEqualTo(expectedDetails); @@ -162,4 +171,11 @@ class NodeControllerTest { assertThat(nodeController.getBalance(PUBKEY)).isEqualTo(BalanceInformationDto.createFrom(BALANCE_INFORMATION)); verify(metrics).mark(argThat(name -> name.endsWith(".getBalance"))); } + + @Test + void getFeeReport() { + when(feeService.getEarnedFeesForPeer(PUBKEY)).thenReturn(Coins.ofMilliSatoshis(1_234)); + assertThat(nodeController.getFeeReport(PUBKEY)).isEqualTo(new FeeReportDto("1234")); + verify(metrics).mark(argThat(name -> name.endsWith(".getFeeReport"))); + } } \ No newline at end of file