diff --git a/application/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java b/application/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java index dd79032b..a9cf5c1d 100644 --- a/application/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java +++ b/application/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java @@ -1,8 +1,11 @@ package de.cotto.lndmanagej.controller; import de.cotto.lndmanagej.metrics.Metrics; +import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.service.BalanceService; import de.cotto.lndmanagej.service.ChannelService; import de.cotto.lndmanagej.service.NodeService; +import de.cotto.lndmanagej.service.OnChainCostService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -11,6 +14,7 @@ import org.springframework.test.web.servlet.MockMvc; import java.util.Optional; +import static de.cotto.lndmanagej.model.BalanceInformationFixtures.BALANCE_INFORMATION_2; import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_PRIVATE; import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS_2; @@ -38,6 +42,12 @@ class ChannelDetailsControllerIT { @SuppressWarnings("unused") private Metrics metrics; + @MockBean + private OnChainCostService onChainCostService; + + @MockBean + private BalanceService balanceService; + @Test void not_found() throws Exception { mockMvc.perform(get(CHANNEL_PREFIX + "/details")) @@ -48,10 +58,28 @@ class ChannelDetailsControllerIT { void getChannelDetails() throws Exception { when(nodeService.getAlias(PUBKEY_2)).thenReturn(ALIAS_2); when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL_PRIVATE)); + 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)); mockMvc.perform(get(CHANNEL_PREFIX + "/details")) - .andExpect(jsonPath("$.channelId", is(String.valueOf(CHANNEL_ID.getShortChannelId())))) + .andExpect(jsonPath("$.channelId", is(CHANNEL_ID.toString()))) .andExpect(jsonPath("$.remotePubkey", is(PUBKEY_2.toString()))) .andExpect(jsonPath("$.remoteAlias", is(ALIAS_2))) - .andExpect(jsonPath("$.private", is(true))); + .andExpect(jsonPath("$.private", is(true))) + .andExpect(jsonPath("$.onChainCosts.openCosts", is("1000"))) + .andExpect(jsonPath("$.onChainCosts.closeCosts", is("2000"))) + .andExpect(jsonPath("$.balance.localBalance", is("2000"))) + .andExpect(jsonPath("$.balance.localReserve", is("200"))) + .andExpect(jsonPath("$.balance.localAvailable", is("1800"))) + .andExpect(jsonPath("$.balance.remoteBalance", is("223"))) + .andExpect(jsonPath("$.balance.remoteReserve", is("20"))) + .andExpect(jsonPath("$.balance.remoteAvailable", is("203"))); + } + + @Test + void getChannelDetails_channel_not_found() throws Exception { + when(nodeService.getAlias(PUBKEY_2)).thenReturn(ALIAS_2); + mockMvc.perform(get(CHANNEL_PREFIX + "/details")) + .andExpect(status().isNotFound()); } } \ No newline at end of file diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java b/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java index 33bd466f..b6f00b64 100644 --- a/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java +++ b/application/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java @@ -3,12 +3,17 @@ package de.cotto.lndmanagej.controller; import com.codahale.metrics.MetricRegistry; import de.cotto.lndmanagej.controller.dto.ChannelDetailsDto; import de.cotto.lndmanagej.controller.dto.ObjectMapperConfiguration; +import de.cotto.lndmanagej.controller.dto.OnChainCostsDto; import de.cotto.lndmanagej.metrics.Metrics; +import de.cotto.lndmanagej.model.BalanceInformation; import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.LocalChannel; import de.cotto.lndmanagej.model.Pubkey; +import de.cotto.lndmanagej.service.BalanceService; import de.cotto.lndmanagej.service.ChannelService; import de.cotto.lndmanagej.service.NodeService; +import de.cotto.lndmanagej.service.OnChainCostService; import org.springframework.context.annotation.Import; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -22,10 +27,20 @@ public class ChannelDetailsController { private final ChannelService channelService; private final NodeService nodeService; private final Metrics metrics; + private final BalanceService balanceService; + private final OnChainCostService onChainCostService; - public ChannelDetailsController(ChannelService channelService, NodeService nodeService, Metrics metrics) { + public ChannelDetailsController( + ChannelService channelService, + NodeService nodeService, + BalanceService balanceService, + OnChainCostService onChainCostService, + Metrics metrics + ) { this.channelService = channelService; this.nodeService = nodeService; + this.balanceService = balanceService; + this.onChainCostService = onChainCostService; this.metrics = metrics; } @@ -38,7 +53,22 @@ public class ChannelDetailsController { } Pubkey remotePubkey = localChannel.getRemotePubkey(); String remoteAlias = nodeService.getAlias(remotePubkey); - boolean privateChannel = localChannel.isPrivateChannel(); - return new ChannelDetailsDto(localChannel.getId(), remotePubkey, remoteAlias, privateChannel); + return new ChannelDetailsDto( + localChannel, + remoteAlias, + getBalanceInformation(channelId), + getOnChainCosts(channelId) + ); + } + + private BalanceInformation getBalanceInformation(ChannelId channelId) { + return balanceService.getBalanceInformation(channelId) + .orElse(BalanceInformation.EMPTY); + } + + private OnChainCostsDto getOnChainCosts(ChannelId channelId) { + Coins openCosts = onChainCostService.getOpenCosts(channelId).orElse(Coins.NONE); + Coins closeCosts = onChainCostService.getCloseCosts(channelId).orElse(Coins.NONE); + return new OnChainCostsDto(openCosts, closeCosts); } } diff --git a/application/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java b/application/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java index d12c7a8e..8315cea9 100644 --- a/application/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java +++ b/application/src/main/java/de/cotto/lndmanagej/controller/dto/ChannelDetailsDto.java @@ -1,7 +1,9 @@ package de.cotto.lndmanagej.controller.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import de.cotto.lndmanagej.model.BalanceInformation; import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.LocalChannel; import de.cotto.lndmanagej.model.Pubkey; public record ChannelDetailsDto( @@ -10,16 +12,25 @@ public record ChannelDetailsDto( String channelIdCompactLnd, Pubkey remotePubkey, String remoteAlias, - @JsonProperty("private") boolean privateChannel + @JsonProperty("private") boolean privateChannel, + BalanceInformationDto balance, + OnChainCostsDto onChainCosts ) { - public ChannelDetailsDto(ChannelId channelId, Pubkey remotePubkey, String remoteAlias, boolean privateChannel) { + public ChannelDetailsDto( + LocalChannel localChannel, + String remoteAlias, + BalanceInformation balanceInformation, + OnChainCostsDto onChainCosts + ) { this( - channelId, - channelId.getCompactForm(), - channelId.getCompactFormLnd(), - remotePubkey, + localChannel.getId(), + localChannel.getId().getCompactForm(), + localChannel.getId().getCompactFormLnd(), + localChannel.getRemotePubkey(), remoteAlias, - privateChannel + localChannel.isPrivateChannel(), + BalanceInformationDto.createFrom(balanceInformation), + onChainCosts ); } } diff --git a/application/src/main/java/de/cotto/lndmanagej/service/BalanceService.java b/application/src/main/java/de/cotto/lndmanagej/service/BalanceService.java index 199cd59a..65fe43ef 100644 --- a/application/src/main/java/de/cotto/lndmanagej/service/BalanceService.java +++ b/application/src/main/java/de/cotto/lndmanagej/service/BalanceService.java @@ -55,7 +55,7 @@ public class BalanceService { .reduce(BalanceInformation.EMPTY, BalanceInformation::add); } - private Optional getBalanceInformation(ChannelId channelId) { + public Optional getBalanceInformation(ChannelId channelId) { return grpcChannels.getChannel(channelId) .map(LocalOpenChannel::getBalanceInformation); } diff --git a/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java b/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java index 6a25555b..16dd1513 100644 --- a/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java +++ b/application/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java @@ -1,9 +1,14 @@ package de.cotto.lndmanagej.controller; import de.cotto.lndmanagej.controller.dto.ChannelDetailsDto; +import de.cotto.lndmanagej.controller.dto.OnChainCostsDto; import de.cotto.lndmanagej.metrics.Metrics; +import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.service.BalanceService; import de.cotto.lndmanagej.service.ChannelService; import de.cotto.lndmanagej.service.NodeService; +import de.cotto.lndmanagej.service.OnChainCostService; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -20,11 +25,15 @@ import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ChannelDetailsControllerTest { + private static final Coins OPEN_COSTS = Coins.ofSatoshis(1); + private static final Coins CLOSE_COSTS = Coins.ofSatoshis(2); + private static final OnChainCostsDto ON_CHAIN_COSTS = new OnChainCostsDto(OPEN_COSTS, CLOSE_COSTS); @InjectMocks private ChannelDetailsController channelDetailsController; @@ -37,6 +46,18 @@ class ChannelDetailsControllerTest { @Mock private Metrics metrics; + @Mock + private BalanceService balanceService; + + @Mock + private OnChainCostService onChainCostService; + + @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)); + } + @Test void getDetails_channel_not_found() { assertThatExceptionOfType(NotFoundException.class) @@ -45,9 +66,16 @@ class ChannelDetailsControllerTest { @Test void getDetails() throws NotFoundException { - ChannelDetailsDto expectedDetails = new ChannelDetailsDto(CHANNEL_ID, PUBKEY_2, ALIAS_2, false); + ChannelDetailsDto expectedDetails = new ChannelDetailsDto( + LOCAL_OPEN_CHANNEL, + ALIAS_2, + LOCAL_OPEN_CHANNEL.getBalanceInformation(), + ON_CHAIN_COSTS + ); when(nodeService.getAlias(PUBKEY_2)).thenReturn(ALIAS_2); when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL)); + when(balanceService.getBalanceInformation(CHANNEL_ID)) + .thenReturn(Optional.ofNullable(LOCAL_OPEN_CHANNEL.getBalanceInformation())); assertThat(channelDetailsController.getDetails(CHANNEL_ID)).isEqualTo(expectedDetails); verify(metrics).mark(argThat(name -> name.endsWith(".getDetails"))); @@ -55,9 +83,16 @@ class ChannelDetailsControllerTest { @Test void getDetails_private() throws NotFoundException { - ChannelDetailsDto expectedDetails = new ChannelDetailsDto(CHANNEL_ID, PUBKEY_2, ALIAS_2, true); + ChannelDetailsDto expectedDetails = new ChannelDetailsDto( + LOCAL_OPEN_CHANNEL_PRIVATE, + ALIAS_2, + LOCAL_OPEN_CHANNEL_PRIVATE.getBalanceInformation(), + ON_CHAIN_COSTS + ); when(nodeService.getAlias(PUBKEY_2)).thenReturn(ALIAS_2); when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL_PRIVATE)); + when(balanceService.getBalanceInformation(CHANNEL_ID)) + .thenReturn(Optional.ofNullable(LOCAL_OPEN_CHANNEL_PRIVATE.getBalanceInformation())); assertThat(channelDetailsController.getDetails(CHANNEL_ID)).isEqualTo(expectedDetails); verify(metrics).mark(argThat(name -> name.endsWith(".getDetails"))); diff --git a/application/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java b/application/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java index 05b987fc..f02b1fa5 100644 --- a/application/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java +++ b/application/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java @@ -8,6 +8,7 @@ import de.cotto.lndmanagej.metrics.Metrics; import de.cotto.lndmanagej.model.BalanceInformation; import de.cotto.lndmanagej.model.Coins; 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.NodeService; @@ -76,7 +77,7 @@ class NodeControllerTest { void getNodeDetails_no_channels() { when(onChainCostService.getOpenCostsWith(any())).thenReturn(Coins.NONE); when(onChainCostService.getCloseCostsWith(any())).thenReturn(Coins.NONE); - when(balanceService.getBalanceInformation(any())).thenReturn(BalanceInformation.EMPTY); + when(balanceService.getBalanceInformation(any(Pubkey.class))).thenReturn(BalanceInformation.EMPTY); NodeDetailsDto expectedDetails = new NodeDetailsDto( PUBKEY_2, ALIAS_2, diff --git a/application/src/test/java/de/cotto/lndmanagej/service/BalanceServiceTest.java b/application/src/test/java/de/cotto/lndmanagej/service/BalanceServiceTest.java index 41740046..108a7866 100644 --- a/application/src/test/java/de/cotto/lndmanagej/service/BalanceServiceTest.java +++ b/application/src/test/java/de/cotto/lndmanagej/service/BalanceServiceTest.java @@ -36,10 +36,10 @@ class BalanceServiceTest { @Test void getBalanceInformation_for_pubkey() { BalanceInformation expected = new BalanceInformation( - Coins.ofSatoshis(2_000), - Coins.ofSatoshis(200), - Coins.ofSatoshis(246), - Coins.ofSatoshis(20) + Coins.ofSatoshis(3_000), + Coins.ofSatoshis(300), + Coins.ofSatoshis(346), + Coins.ofSatoshis(30) ); when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, LOCAL_OPEN_CHANNEL_2)); when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL_MORE_BALANCE)); @@ -47,6 +47,19 @@ class BalanceServiceTest { assertThat(balanceService.getBalanceInformation(PUBKEY)).isEqualTo(expected); } + @Test + void getBalanceInformation_for_channel() { + when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL_MORE_BALANCE)); + assertThat(balanceService.getBalanceInformation(CHANNEL_ID)) + .contains(LOCAL_OPEN_CHANNEL_MORE_BALANCE.getBalanceInformation()); + } + + @Test + void getBalanceInformation_for_channel_empty() { + when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.empty()); + assertThat(balanceService.getBalanceInformation(CHANNEL_ID)).isEmpty(); + } + @Test void getAvailableLocalBalance_channel() { when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL)); diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/BalanceInformationFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/BalanceInformationFixtures.java index a2c3a84b..bb72453f 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/BalanceInformationFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/BalanceInformationFixtures.java @@ -6,10 +6,10 @@ public class BalanceInformationFixtures { public static final Coins REMOTE_BALANCE = Coins.ofSatoshis(123); public static final Coins REMOTE_RESERVE = Coins.ofSatoshis(10); - public static final Coins LOCAL_BALANCE_2 = Coins.ofSatoshis(1_000); - public static final Coins LOCAL_RESERVE_2 = Coins.ofSatoshis(100); - public static final Coins REMOTE_BALANCE_2 = Coins.ofSatoshis(123); - public static final Coins REMOTE_RESERVE_2 = Coins.ofSatoshis(10); + public static final Coins LOCAL_BALANCE_2 = Coins.ofSatoshis(2_000); + public static final Coins LOCAL_RESERVE_2 = Coins.ofSatoshis(200); + public static final Coins REMOTE_BALANCE_2 = Coins.ofSatoshis(223); + public static final Coins REMOTE_RESERVE_2 = Coins.ofSatoshis(20); public static final BalanceInformation BALANCE_INFORMATION = new BalanceInformation(LOCAL_BALANCE, LOCAL_RESERVE, REMOTE_BALANCE, REMOTE_RESERVE);