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 80ef9604..581a3a91 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/FeeService.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/FeeService.java @@ -3,6 +3,7 @@ package de.cotto.lndmanagej.service; import de.cotto.lndmanagej.grpc.GrpcFees; import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.FeeConfiguration; import org.springframework.stereotype.Component; @Component @@ -28,4 +29,13 @@ public class FeeService { public Coins getIncomingBaseFee(ChannelId channelId) { return grpcFees.getIncomingBaseFee(channelId).orElseThrow(IllegalStateException::new); } + + public FeeConfiguration getFeeConfiguration(ChannelId channelId) { + return new FeeConfiguration( + getOutgoingFeeRate(channelId), + getOutgoingBaseFee(channelId), + getIncomingFeeRate(channelId), + getIncomingBaseFee(channelId) + ); + } } 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 a51b81ae..fc10336d 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/FeeServiceTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/FeeServiceTest.java @@ -2,6 +2,7 @@ package de.cotto.lndmanagej.service; import de.cotto.lndmanagej.grpc.GrpcFees; import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.FeeConfiguration; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -23,6 +24,29 @@ class FeeServiceTest { @Mock private GrpcFees grpcFees; + @Test + void getFeeConfiguration() { + FeeConfiguration expected = new FeeConfiguration( + 789, + Coins.ofMilliSatoshis(111), + 123, + Coins.ofMilliSatoshis(456) + ); + + when(grpcFees.getOutgoingFeeRate(CHANNEL_ID)).thenReturn(Optional.of(789L)); + when(grpcFees.getOutgoingBaseFee(CHANNEL_ID)).thenReturn(Optional.of(Coins.ofMilliSatoshis(111))); + when(grpcFees.getIncomingFeeRate(CHANNEL_ID)).thenReturn(Optional.of(123L)); + when(grpcFees.getIncomingBaseFee(CHANNEL_ID)).thenReturn(Optional.of(Coins.ofMilliSatoshis(456))); + + assertThat(feeService.getFeeConfiguration(CHANNEL_ID)) + .isEqualTo(expected); + } + + @Test + void getFeeConfiguration_not_found() { + assertThatIllegalStateException().isThrownBy(() -> feeService.getFeeConfiguration(CHANNEL_ID)); + } + @Test void getIncomingFeeRate() { when(grpcFees.getIncomingFeeRate(CHANNEL_ID)).thenReturn(Optional.of(123L)); diff --git a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannelPolicy.java b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannelPolicy.java index cd68b74f..8b31a137 100644 --- a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannelPolicy.java +++ b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannelPolicy.java @@ -1,24 +1,32 @@ package de.cotto.lndmanagej.grpc; +import com.github.benmanes.caffeine.cache.LoadingCache; +import de.cotto.lndmanagej.caching.CacheBuilder; import de.cotto.lndmanagej.model.ChannelId; +import lnrpc.ChannelEdge; import lnrpc.RoutingPolicy; import org.springframework.stereotype.Component; +import java.time.Duration; import java.util.Optional; @Component public class GrpcChannelPolicy { private final GrpcService grpcService; private final GrpcGetInfo grpcGetInfo; + private final LoadingCache> channelEdgeCache; public GrpcChannelPolicy(GrpcService grpcService, GrpcGetInfo grpcGetInfo) { this.grpcService = grpcService; this.grpcGetInfo = grpcGetInfo; + channelEdgeCache = new CacheBuilder() + .withExpiry(Duration.ofMinutes(1)) + .build(this::getChannelEdgeWithoutCache); } public Optional getLocalPolicy(ChannelId channelId) { String ownPubkey = grpcGetInfo.getPubkey().toString(); - return grpcService.getChannelEdge(channelId).map( + return getChannelEdge(channelId).map( channelEdge -> { if (ownPubkey.equals(channelEdge.getNode1Pub())) { return channelEdge.getNode1Policy(); @@ -33,7 +41,7 @@ public class GrpcChannelPolicy { public Optional getRemotePolicy(ChannelId channelId) { String ownPubkey = grpcGetInfo.getPubkey().toString(); - return grpcService.getChannelEdge(channelId).map( + return getChannelEdge(channelId).map( channelEdge -> { if (ownPubkey.equals(channelEdge.getNode2Pub())) { return channelEdge.getNode1Policy(); @@ -46,4 +54,12 @@ public class GrpcChannelPolicy { ); } + private Optional getChannelEdge(ChannelId channelId) { + return channelEdgeCache.get(channelId); + } + + private Optional getChannelEdgeWithoutCache(ChannelId channelId) { + return grpcService.getChannelEdge(channelId); + } + } diff --git a/model/src/main/java/de/cotto/lndmanagej/model/FeeConfiguration.java b/model/src/main/java/de/cotto/lndmanagej/model/FeeConfiguration.java new file mode 100644 index 00000000..1e383d2d --- /dev/null +++ b/model/src/main/java/de/cotto/lndmanagej/model/FeeConfiguration.java @@ -0,0 +1,9 @@ +package de.cotto.lndmanagej.model; + +public record FeeConfiguration( + long outgoingFeeRate, + Coins outgoingBaseFee, + long incomingFeeRate, + Coins incomingBaseFee +) { +} diff --git a/model/src/test/java/de/cotto/lndmanagej/model/FeeConfigurationTest.java b/model/src/test/java/de/cotto/lndmanagej/model/FeeConfigurationTest.java new file mode 100644 index 00000000..fe7ef960 --- /dev/null +++ b/model/src/test/java/de/cotto/lndmanagej/model/FeeConfigurationTest.java @@ -0,0 +1,30 @@ +package de.cotto.lndmanagej.model; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FeeConfigurationTest { + private final FeeConfiguration feeConfiguration = + new FeeConfiguration(10, Coins.ofMilliSatoshis(20), 30, Coins.ofMilliSatoshis(40)); + + @Test + void outgoingFeeRate() { + assertThat(feeConfiguration.outgoingFeeRate()).isEqualTo(10L); + } + + @Test + void outgoingBaseFee() { + assertThat(feeConfiguration.outgoingBaseFee()).isEqualTo(Coins.ofMilliSatoshis(20)); + } + + @Test + void incomingFeeRate() { + assertThat(feeConfiguration.incomingFeeRate()).isEqualTo(30L); + } + + @Test + void incomingBaseFee() { + assertThat(feeConfiguration.incomingBaseFee()).isEqualTo(Coins.ofMilliSatoshis(40)); + } +} \ No newline at end of file diff --git a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java index 37b11e01..072ca7c9 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerIT.java @@ -2,8 +2,10 @@ package de.cotto.lndmanagej.controller; import de.cotto.lndmanagej.metrics.Metrics; import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.FeeConfiguration; 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; @@ -30,6 +32,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @WebMvcTest(controllers = ChannelDetailsController.class) class ChannelDetailsControllerIT { private static final String CHANNEL_PREFIX = "/api/channel/" + CHANNEL_ID.getShortChannelId(); + private static final FeeConfiguration FEE_CONFIGURATION = + new FeeConfiguration(1, Coins.ofMilliSatoshis(2), 3, Coins.ofMilliSatoshis(4)); @Autowired private MockMvc mockMvc; @@ -50,6 +54,9 @@ class ChannelDetailsControllerIT { @MockBean private BalanceService balanceService; + @MockBean + private FeeService feeService; + @Test void not_found() throws Exception { mockMvc.perform(get(CHANNEL_PREFIX + "/details")) @@ -58,6 +65,7 @@ class ChannelDetailsControllerIT { @Test void getChannelDetails() throws Exception { + when(feeService.getFeeConfiguration(CHANNEL_ID)).thenReturn(FEE_CONFIGURATION); 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))); @@ -83,7 +91,11 @@ class ChannelDetailsControllerIT { .andExpect(jsonPath("$.balance.localAvailable", is("1800"))) .andExpect(jsonPath("$.balance.remoteBalance", is("223"))) .andExpect(jsonPath("$.balance.remoteReserve", is("20"))) - .andExpect(jsonPath("$.balance.remoteAvailable", is("203"))); + .andExpect(jsonPath("$.balance.remoteAvailable", is("203"))) + .andExpect(jsonPath("$.feeConfiguration.outgoingFeeRatePpm", is(1))) + .andExpect(jsonPath("$.feeConfiguration.outgoingBaseFeeMilliSat", is(2))) + .andExpect(jsonPath("$.feeConfiguration.incomingFeeRatePpm", is(3))) + .andExpect(jsonPath("$.feeConfiguration.incomingBaseFeeMilliSat", is(4))); } @Test diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java b/web/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java index b6f00b64..9fbde973 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/ChannelDetailsController.java @@ -2,16 +2,19 @@ package de.cotto.lndmanagej.controller; import com.codahale.metrics.MetricRegistry; import de.cotto.lndmanagej.controller.dto.ChannelDetailsDto; +import de.cotto.lndmanagej.controller.dto.FeeConfigurationDto; 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.FeeConfiguration; 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.FeeService; import de.cotto.lndmanagej.service.NodeService; import de.cotto.lndmanagej.service.OnChainCostService; import org.springframework.context.annotation.Import; @@ -29,12 +32,14 @@ public class ChannelDetailsController { private final Metrics metrics; private final BalanceService balanceService; private final OnChainCostService onChainCostService; + private final FeeService feeService; public ChannelDetailsController( ChannelService channelService, NodeService nodeService, BalanceService balanceService, OnChainCostService onChainCostService, + FeeService feeService, Metrics metrics ) { this.channelService = channelService; @@ -42,6 +47,7 @@ public class ChannelDetailsController { this.balanceService = balanceService; this.onChainCostService = onChainCostService; this.metrics = metrics; + this.feeService = feeService; } @GetMapping("/details") @@ -57,7 +63,8 @@ public class ChannelDetailsController { localChannel, remoteAlias, getBalanceInformation(channelId), - getOnChainCosts(channelId) + getOnChainCosts(channelId), + getFeeConfiguration(channelId) ); } @@ -71,4 +78,9 @@ public class ChannelDetailsController { Coins closeCosts = onChainCostService.getCloseCosts(channelId).orElse(Coins.NONE); return new OnChainCostsDto(openCosts, closeCosts); } + + private FeeConfigurationDto getFeeConfiguration(ChannelId channelId) { + FeeConfiguration feeConfiguration = feeService.getFeeConfiguration(channelId); + return FeeConfigurationDto.createFrom(feeConfiguration); + } } 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 28d575a9..8456244d 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 @@ -16,13 +16,15 @@ public record ChannelDetailsDto( String capacity, ChannelStatusDto status, BalanceInformationDto balance, - OnChainCostsDto onChainCosts + OnChainCostsDto onChainCosts, + FeeConfigurationDto feeConfiguration ) { public ChannelDetailsDto( LocalChannel localChannel, String remoteAlias, BalanceInformation balanceInformation, - OnChainCostsDto onChainCosts + OnChainCostsDto onChainCosts, + FeeConfigurationDto feeConfiguration ) { this( String.valueOf(localChannel.getId().getShortChannelId()), @@ -35,7 +37,8 @@ public record ChannelDetailsDto( String.valueOf(localChannel.getCapacity().satoshis()), ChannelStatusDto.createFrom(localChannel.getStatus()), BalanceInformationDto.createFrom(balanceInformation), - onChainCosts + onChainCosts, + feeConfiguration ); } } diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/dto/FeeConfigurationDto.java b/web/src/main/java/de/cotto/lndmanagej/controller/dto/FeeConfigurationDto.java new file mode 100644 index 00000000..61d38cba --- /dev/null +++ b/web/src/main/java/de/cotto/lndmanagej/controller/dto/FeeConfigurationDto.java @@ -0,0 +1,19 @@ +package de.cotto.lndmanagej.controller.dto; + +import de.cotto.lndmanagej.model.FeeConfiguration; + +public record FeeConfigurationDto( + long outgoingFeeRatePpm, + long outgoingBaseFeeMilliSat, + long incomingFeeRatePpm, + long incomingBaseFeeMilliSat +) { + public static FeeConfigurationDto createFrom(FeeConfiguration feeConfiguration) { + return new FeeConfigurationDto( + feeConfiguration.outgoingFeeRate(), + feeConfiguration.outgoingBaseFee().milliSatoshis(), + feeConfiguration.incomingFeeRate(), + feeConfiguration.incomingBaseFee().milliSatoshis() + ); + } +} diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java index 16dd1513..5502120d 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/ChannelDetailsControllerTest.java @@ -1,11 +1,14 @@ package de.cotto.lndmanagej.controller; import de.cotto.lndmanagej.controller.dto.ChannelDetailsDto; +import de.cotto.lndmanagej.controller.dto.FeeConfigurationDto; import de.cotto.lndmanagej.controller.dto.OnChainCostsDto; import de.cotto.lndmanagej.metrics.Metrics; import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.FeeConfiguration; 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.BeforeEach; @@ -34,6 +37,10 @@ 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); + private static final FeeConfiguration FEE_CONFIGURATION = + new FeeConfiguration(1, Coins.ofMilliSatoshis(2), 3, Coins.ofMilliSatoshis(4)); + private static final FeeConfigurationDto FEE_CONFIGURATION_DTO = FeeConfigurationDto.createFrom(FEE_CONFIGURATION); + @InjectMocks private ChannelDetailsController channelDetailsController; @@ -52,10 +59,14 @@ class ChannelDetailsControllerTest { @Mock private OnChainCostService onChainCostService; + @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(feeService.getFeeConfiguration(CHANNEL_ID)).thenReturn(FEE_CONFIGURATION); } @Test @@ -70,7 +81,8 @@ class ChannelDetailsControllerTest { LOCAL_OPEN_CHANNEL, ALIAS_2, LOCAL_OPEN_CHANNEL.getBalanceInformation(), - ON_CHAIN_COSTS + ON_CHAIN_COSTS, + FEE_CONFIGURATION_DTO ); when(nodeService.getAlias(PUBKEY_2)).thenReturn(ALIAS_2); when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL)); @@ -87,7 +99,8 @@ class ChannelDetailsControllerTest { LOCAL_OPEN_CHANNEL_PRIVATE, ALIAS_2, LOCAL_OPEN_CHANNEL_PRIVATE.getBalanceInformation(), - ON_CHAIN_COSTS + ON_CHAIN_COSTS, + FEE_CONFIGURATION_DTO ); when(nodeService.getAlias(PUBKEY_2)).thenReturn(ALIAS_2); when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL_PRIVATE)); 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 a5e4f1fc..acde09bd 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 @@ -18,8 +18,10 @@ import static org.assertj.core.api.Assertions.assertThat; class ChannelDetailsDtoTest { private static final OnChainCostsDto ON_CHAIN_COSTS = new OnChainCostsDto(Coins.ofSatoshis(1), Coins.ofSatoshis(2)); + private static final FeeConfigurationDto FEE_CONFIGURATION_DTO = + new FeeConfigurationDto(0, 0, 0, 0); private static final ChannelDetailsDto CHANNEL_DETAILS_DTO = - new ChannelDetailsDto(CLOSED_CHANNEL, ALIAS, BALANCE_INFORMATION, ON_CHAIN_COSTS); + new ChannelDetailsDto(CLOSED_CHANNEL, ALIAS, BALANCE_INFORMATION, ON_CHAIN_COSTS, FEE_CONFIGURATION_DTO); @Test void channelIdShort() { @@ -63,8 +65,13 @@ class ChannelDetailsDtoTest { @Test void status() { - ChannelDetailsDto dto = - new ChannelDetailsDto(LOCAL_OPEN_CHANNEL, ALIAS, BALANCE_INFORMATION, ON_CHAIN_COSTS); + ChannelDetailsDto dto = new ChannelDetailsDto( + LOCAL_OPEN_CHANNEL, + ALIAS, + BALANCE_INFORMATION, + ON_CHAIN_COSTS, + FEE_CONFIGURATION_DTO + ); ChannelStatusDto channelStatusDto = ChannelStatusDto.createFrom(new ChannelStatus(false, true, false, OPEN)); assertThat(dto.status()).isEqualTo(channelStatusDto); diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/dto/FeeConfigurationDtoTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/dto/FeeConfigurationDtoTest.java new file mode 100644 index 00000000..39712fce --- /dev/null +++ b/web/src/test/java/de/cotto/lndmanagej/controller/dto/FeeConfigurationDtoTest.java @@ -0,0 +1,20 @@ +package de.cotto.lndmanagej.controller.dto; + +import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.FeeConfiguration; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FeeConfigurationDtoTest { + @Test + void createFrom() { + FeeConfigurationDto expected = new FeeConfigurationDto(1, 2, 3, 4); + + FeeConfiguration feeConfiguration = + new FeeConfiguration(1, Coins.ofMilliSatoshis(2), 3, Coins.ofMilliSatoshis(4)); + FeeConfigurationDto dto = FeeConfigurationDto.createFrom(feeConfiguration); + + assertThat(dto).isEqualTo(expected); + } +} \ No newline at end of file