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 d28ff20c..a234f1af 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelControllerIT.java @@ -5,6 +5,7 @@ import de.cotto.lndmanagej.model.ChannelIdResolver; import de.cotto.lndmanagej.model.Coins; 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 de.cotto.lndmanagej.service.PolicyService; @@ -70,6 +71,9 @@ class ChannelControllerIT { @MockBean private PolicyService policyService; + @MockBean + private FeeService feeService; + @Test void getBasicInformation_not_found() throws Exception { mockMvc.perform(get(CHANNEL_PREFIX + "/")) @@ -122,6 +126,7 @@ class ChannelControllerIT { when(onChainCostService.getOpenCosts(CHANNEL_ID)).thenReturn(Optional.of(Coins.ofSatoshis(1000))); 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)); mockMvc.perform(get(DETAILS_PREFIX)) .andExpect(jsonPath("$.channelIdShort", is(String.valueOf(CHANNEL_ID.getShortChannelId())))) .andExpect(jsonPath("$.channelIdCompact", is(CHANNEL_ID.getCompactForm()))) @@ -151,12 +156,14 @@ class ChannelControllerIT { .andExpect(jsonPath("$.policies.local.feeRatePpm", is(100))) .andExpect(jsonPath("$.policies.local.baseFeeMilliSat", is(10))) .andExpect(jsonPath("$.policies.remote.feeRatePpm", is(222))) - .andExpect(jsonPath("$.policies.remote.baseFeeMilliSat", is(0))); + .andExpect(jsonPath("$.policies.remote.baseFeeMilliSat", is(0))) + .andExpect(jsonPath("$.feeReport.earned", is("1234"))); } @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); mockMvc.perform(get(DETAILS_PREFIX)) .andExpect(jsonPath("$.closeDetails.initiator", is("REMOTE"))) .andExpect(jsonPath("$.closeDetails.height", is(987_654))) @@ -214,4 +221,11 @@ class ChannelControllerIT { .andExpect(jsonPath("$.initiator", is("REMOTE"))) .andExpect(jsonPath("$.height", is(987_654))); } + + @Test + void getFeeReport() throws Exception { + when(feeService.getEarnedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(1_234)); + mockMvc.perform(get(CHANNEL_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/ChannelController.java b/web/src/main/java/de/cotto/lndmanagej/controller/ChannelController.java index 0581674a..2a5e89e3 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/ChannelController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/ChannelController.java @@ -5,6 +5,7 @@ import de.cotto.lndmanagej.controller.dto.BalanceInformationDto; import de.cotto.lndmanagej.controller.dto.ChannelDetailsDto; import de.cotto.lndmanagej.controller.dto.ChannelDto; import de.cotto.lndmanagej.controller.dto.ClosedChannelDetailsDto; +import de.cotto.lndmanagej.controller.dto.FeeReportDto; import de.cotto.lndmanagej.controller.dto.ObjectMapperConfiguration; import de.cotto.lndmanagej.controller.dto.OnChainCostsDto; import de.cotto.lndmanagej.controller.dto.PoliciesDto; @@ -19,6 +20,7 @@ import de.cotto.lndmanagej.model.Policies; 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 de.cotto.lndmanagej.service.PolicyService; @@ -31,8 +33,9 @@ import org.springframework.web.bind.annotation.RestController; import javax.annotation.Nullable; @RestController -@RequestMapping("/api/channel/{channelId}") @Import(ObjectMapperConfiguration.class) +@SuppressWarnings("PMD.ExcessiveImports") +@RequestMapping("/api/channel/{channelId}") public class ChannelController { private final ChannelService channelService; private final NodeService nodeService; @@ -40,6 +43,7 @@ public class ChannelController { private final BalanceService balanceService; private final OnChainCostService onChainCostService; private final PolicyService policyService; + private final FeeService feeService; public ChannelController( ChannelService channelService, @@ -47,14 +51,16 @@ public class ChannelController { BalanceService balanceService, OnChainCostService onChainCostService, PolicyService policyService, + FeeService feeService, Metrics metrics ) { this.channelService = channelService; this.nodeService = nodeService; this.balanceService = balanceService; this.onChainCostService = onChainCostService; - this.metrics = metrics; this.policyService = policyService; + this.feeService = feeService; + this.metrics = metrics; } @GetMapping("/") @@ -83,7 +89,8 @@ public class ChannelController { getBalanceInformation(channelId), getOnChainCosts(channelId), getPoliciesForChannel(localChannel), - getCloseDetailsForChannel(localChannel) + getCloseDetailsForChannel(localChannel), + getFeeReportDto(localChannel.getId()) ); } @@ -112,6 +119,17 @@ public class ChannelController { return new ClosedChannelDetailsDto(closedChannel.getCloseInitiator(), closedChannel.getCloseHeight()); } + @GetMapping("/fee-report") + public FeeReportDto getFeeReport(@PathVariable ChannelId channelId) { + mark("getFeeReport"); + return getFeeReportDto(channelId); + } + + private FeeReportDto getFeeReportDto(ChannelId channelId) { + Coins earned = feeService.getEarnedFeesForChannel(channelId); + return new FeeReportDto(earned); + } + private PoliciesDto getPoliciesForChannel(@Nullable LocalChannel channel) { if (channel == null || channel.getStatus().openCloseStatus() != OpenCloseStatus.OPEN) { return PoliciesDto.EMPTY; diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java b/web/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java index b2dabeff..9d1cd272 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java @@ -22,14 +22,16 @@ public record ChannelDetailsDto( BalanceInformationDto balance, OnChainCostsDto onChainCosts, PoliciesDto policies, - ClosedChannelDetailsDto closeDetails + ClosedChannelDetailsDto closeDetails, + FeeReportDto feeReport ) { public ChannelDetailsDto( ChannelDto channelDto, String remoteAlias, BalanceInformation balanceInformation, OnChainCostsDto onChainCosts, - PoliciesDto policies + PoliciesDto policies, + FeeReportDto feeReport ) { this( channelDto.channelIdShort(), @@ -47,7 +49,8 @@ public record ChannelDetailsDto( BalanceInformationDto.createFrom(balanceInformation), onChainCosts, policies, - channelDto.closeDetails() + channelDto.closeDetails(), + feeReport ); } @@ -57,14 +60,16 @@ public record ChannelDetailsDto( BalanceInformation balanceInformation, OnChainCostsDto onChainCosts, PoliciesDto policies, - ClosedChannelDetailsDto closeDetails + ClosedChannelDetailsDto closeDetails, + FeeReportDto feeReport ) { this( new ChannelDto(localChannel, closeDetails), remoteAlias, balanceInformation, onChainCosts, - policies + policies, + feeReport ); } } 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 new file mode 100644 index 00000000..c2bce112 --- /dev/null +++ b/web/src/main/java/de/cotto/lndmanagej/controller/dto/FeeReportDto.java @@ -0,0 +1,9 @@ +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())); + } +} 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 a8e88800..fb17433f 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/ChannelControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/ChannelControllerTest.java @@ -4,6 +4,7 @@ import de.cotto.lndmanagej.controller.dto.BalanceInformationDto; import de.cotto.lndmanagej.controller.dto.ChannelDetailsDto; import de.cotto.lndmanagej.controller.dto.ChannelDto; import de.cotto.lndmanagej.controller.dto.ClosedChannelDetailsDto; +import de.cotto.lndmanagej.controller.dto.FeeReportDto; import de.cotto.lndmanagej.controller.dto.OnChainCostsDto; import de.cotto.lndmanagej.controller.dto.PoliciesDto; import de.cotto.lndmanagej.metrics.Metrics; @@ -13,6 +14,7 @@ import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.LocalChannel; 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 de.cotto.lndmanagej.service.PolicyService; @@ -47,6 +49,7 @@ 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 ClosedChannelDetailsDto CLOSED_CHANNEL_DETAILS_DTO = new ClosedChannelDetailsDto(CloseInitiator.REMOTE, 987_654); @@ -71,11 +74,15 @@ class ChannelControllerTest { @Mock private PolicyService policyService; + @Mock + private FeeService feeService; + @BeforeEach void setUp() { lenient().when(onChainCostService.getOpenCosts(CHANNEL_ID)).thenReturn(Optional.of(OPEN_COSTS)); 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)); } @Test @@ -114,7 +121,8 @@ class ChannelControllerTest { LOCAL_OPEN_CHANNEL.getBalanceInformation(), ON_CHAIN_COSTS, FEE_CONFIGURATION_DTO, - ClosedChannelDetailsDto.UNKNOWN + ClosedChannelDetailsDto.UNKNOWN, + FEE_REPORT_DTO ); when(nodeService.getAlias(PUBKEY_2)).thenReturn(ALIAS_2); when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL)); @@ -133,7 +141,8 @@ class ChannelControllerTest { LOCAL_OPEN_CHANNEL_PRIVATE.getBalanceInformation(), ON_CHAIN_COSTS, FEE_CONFIGURATION_DTO, - ClosedChannelDetailsDto.UNKNOWN + ClosedChannelDetailsDto.UNKNOWN, + FEE_REPORT_DTO ); when(nodeService.getAlias(PUBKEY_2)).thenReturn(ALIAS_2); when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL_PRIVATE)); @@ -201,6 +210,13 @@ class ChannelControllerTest { .isThrownBy(() -> channelController.getCloseDetails(CHANNEL_ID)); } + @Test + void getFeeReport() { + when(feeService.getEarnedFeesForChannel(CHANNEL_ID)).thenReturn(Coins.ofSatoshis(123)); + assertThat(channelController.getFeeReport(CHANNEL_ID)).isEqualTo(new FeeReportDto(Coins.ofSatoshis(123))); + verify(metrics).mark(argThat(name -> name.endsWith(".getFeeReport"))); + } + private ChannelDetailsDto mockForChannelWithoutPolicies( LocalChannel channel, String closeInitiator, @@ -215,7 +231,8 @@ class ChannelControllerTest { BalanceInformation.EMPTY, ON_CHAIN_COSTS, PoliciesDto.EMPTY, - new ClosedChannelDetailsDto(closeInitiator, closeHeight) + new ClosedChannelDetailsDto(closeInitiator, closeHeight), + FEE_REPORT_DTO ); } } \ 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 dbd2f75a..356881c2 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 @@ -26,7 +26,8 @@ class ChannelDetailsDtoTest { BALANCE_INFORMATION, ON_CHAIN_COSTS, PoliciesDto.EMPTY, - CLOSE_DETAILS + CLOSE_DETAILS, + new FeeReportDto(Coins.ofMilliSatoshis(1234)) ); @Test @@ -82,7 +83,8 @@ class ChannelDetailsDtoTest { BALANCE_INFORMATION, ON_CHAIN_COSTS, PoliciesDto.EMPTY, - CLOSE_DETAILS + CLOSE_DETAILS, + new FeeReportDto(Coins.ofMilliSatoshis(1234)) ); ChannelStatusDto channelStatusDto = ChannelStatusDto.createFrom(new ChannelStatus(false, true, false, OPEN)); @@ -99,6 +101,11 @@ class ChannelDetailsDtoTest { assertThat(CHANNEL_DETAILS_DTO.balance()).isEqualTo(BalanceInformationDto.createFrom(BALANCE_INFORMATION)); } + @Test + void feeReport() { + assertThat(CHANNEL_DETAILS_DTO.feeReport()).isEqualTo(new FeeReportDto(Coins.ofMilliSatoshis(1_234))); + } + @Test void onChainCosts() { assertThat(CHANNEL_DETAILS_DTO.onChainCosts()).isEqualTo(ON_CHAIN_COSTS); 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 new file mode 100644 index 00000000..63ba95e6 --- /dev/null +++ b/web/src/test/java/de/cotto/lndmanagej/controller/dto/FeeReportDtoTest.java @@ -0,0 +1,13 @@ +package de.cotto.lndmanagej.controller.dto; + +import de.cotto.lndmanagej.model.Coins; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FeeReportDtoTest { + @Test + void earned() { + assertThat(new FeeReportDto(Coins.ofMilliSatoshis(1_234)).earned()).isEqualTo("1234"); + } +} \ No newline at end of file