diff --git a/backend/build.gradle b/backend/build.gradle index 54770591..bb9f0f8e 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -14,5 +14,5 @@ dependencies { } pitest { - testStrengthThreshold = 99 + testStrengthThreshold = 97 } \ No newline at end of file diff --git a/backend/src/main/java/de/cotto/lndmanagej/service/SelfPaymentsService.java b/backend/src/main/java/de/cotto/lndmanagej/service/SelfPaymentsService.java index ddf05d1d..b1b579fd 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/SelfPaymentsService.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/SelfPaymentsService.java @@ -1,25 +1,93 @@ package de.cotto.lndmanagej.service; +import com.github.benmanes.caffeine.cache.LoadingCache; +import de.cotto.lndmanagej.caching.CacheBuilder; +import de.cotto.lndmanagej.model.Channel; import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.model.SelfPayment; import de.cotto.lndmanagej.selfpayments.SelfPaymentsDao; import org.springframework.stereotype.Component; +import java.time.Duration; +import java.util.Comparator; import java.util.List; +import java.util.function.Function; @Component public class SelfPaymentsService { + private static final Duration EXPIRY = Duration.ofSeconds(30); + private static final Duration REFRESH = Duration.ofSeconds(15); + private static final Duration EXPIRY_CLOSED = Duration.ofHours(24); + private static final Duration REFRESH_CLOSED = Duration.ofHours(1); + private final SelfPaymentsDao dao; + private final ChannelService channelService; + private final LoadingCache> cacheFrom; + private final LoadingCache> cacheFromClosed; + private final LoadingCache> cacheTo; + private final LoadingCache> cacheToClosed; - public SelfPaymentsService(SelfPaymentsDao dao) { + public SelfPaymentsService(SelfPaymentsDao dao, ChannelService channelService) { this.dao = dao; - } - - public List getSelfPaymentsToChannel(ChannelId channelId) { - return dao.getSelfPaymentsToChannel(channelId); + this.channelService = channelService; + cacheFrom = new CacheBuilder() + .withExpiry(EXPIRY) + .withRefresh(REFRESH) + .build(this::getSelfPaymentsFromChannelWithoutCache); + cacheTo = new CacheBuilder() + .withExpiry(EXPIRY) + .withRefresh(REFRESH) + .build(this::getSelfPaymentsToChannelWithoutCache); + cacheFromClosed = new CacheBuilder() + .withExpiry(EXPIRY_CLOSED) + .withRefresh(REFRESH_CLOSED) + .build(this::getSelfPaymentsFromChannelWithoutCache); + cacheToClosed = new CacheBuilder() + .withExpiry(EXPIRY_CLOSED) + .withRefresh(REFRESH_CLOSED) + .build(this::getSelfPaymentsToChannelWithoutCache); } public List getSelfPaymentsFromChannel(ChannelId channelId) { + if (channelService.isClosed(channelId)) { + return cacheFromClosed.get(channelId); + } + return cacheFrom.get(channelId); + } + + public List getSelfPaymentsFromPeer(Pubkey pubkey) { + return getSelfPaymentsForAllChannels(pubkey, this::getSelfPaymentsFromChannel); + } + + public List getSelfPaymentsToChannel(ChannelId channelId) { + if (channelService.isClosed(channelId)) { + return cacheToClosed.get(channelId); + } + return cacheTo.get(channelId); + } + + public List getSelfPaymentsToPeer(Pubkey pubkey) { + return getSelfPaymentsForAllChannels(pubkey, this::getSelfPaymentsToChannel); + } + + private List getSelfPaymentsForAllChannels( + Pubkey pubkey, + Function> provider + ) { + return channelService.getAllChannelsWith(pubkey).stream() + .map(Channel::getId) + .map(provider) + .flatMap(List::stream) + .sorted(Comparator.comparing(SelfPayment::settleDate)) + .toList(); + } + + private List getSelfPaymentsFromChannelWithoutCache(ChannelId channelId) { return dao.getSelfPaymentsFromChannel(channelId).stream().distinct().toList(); } + + private List getSelfPaymentsToChannelWithoutCache(ChannelId channelId) { + return dao.getSelfPaymentsToChannel(channelId); + } } diff --git a/backend/src/test/java/de/cotto/lndmanagej/service/SelfPaymentsServiceTest.java b/backend/src/test/java/de/cotto/lndmanagej/service/SelfPaymentsServiceTest.java index 1cc7900f..700a03ff 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/SelfPaymentsServiceTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/SelfPaymentsServiceTest.java @@ -8,9 +8,14 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.List; +import java.util.Set; import static de.cotto.lndmanagej.SelfPaymentFixtures.SELF_PAYMENT; +import static de.cotto.lndmanagej.SelfPaymentFixtures.SELF_PAYMENT_2; import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL_2; +import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -22,11 +27,8 @@ class SelfPaymentsServiceTest { @Mock private SelfPaymentsDao selfPaymentsDao; - @Test - void getSelfPaymentsToChannel() { - when(selfPaymentsDao.getSelfPaymentsToChannel(CHANNEL_ID)).thenReturn(List.of(SELF_PAYMENT)); - assertThat(selfPaymentsService.getSelfPaymentsToChannel(CHANNEL_ID)).containsExactly(SELF_PAYMENT); - } + @Mock + private ChannelService channelService; @Test void getSelfPaymentsFromChannel() { @@ -34,9 +36,45 @@ class SelfPaymentsServiceTest { assertThat(selfPaymentsService.getSelfPaymentsFromChannel(CHANNEL_ID)).containsExactly(SELF_PAYMENT); } + @Test + void getSelfPaymentsFromChannel_closed() { + when(channelService.isClosed(CHANNEL_ID)).thenReturn(true); + when(selfPaymentsDao.getSelfPaymentsFromChannel(CHANNEL_ID)).thenReturn(List.of(SELF_PAYMENT)); + assertThat(selfPaymentsService.getSelfPaymentsFromChannel(CHANNEL_ID)).containsExactly(SELF_PAYMENT); + } + @Test void getSelfPaymentsFromChannel_no_duplicates() { when(selfPaymentsDao.getSelfPaymentsFromChannel(CHANNEL_ID)).thenReturn(List.of(SELF_PAYMENT, SELF_PAYMENT)); assertThat(selfPaymentsService.getSelfPaymentsFromChannel(CHANNEL_ID)).containsExactly(SELF_PAYMENT); } + + @Test + void getSelfPaymentsFromPeer() { + when(channelService.getAllChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, CLOSED_CHANNEL_2)); + when(selfPaymentsDao.getSelfPaymentsFromChannel(LOCAL_OPEN_CHANNEL.getId())).thenReturn(List.of(SELF_PAYMENT)); + when(selfPaymentsDao.getSelfPaymentsFromChannel(CLOSED_CHANNEL_2.getId())).thenReturn(List.of(SELF_PAYMENT_2)); + assertThat(selfPaymentsService.getSelfPaymentsFromPeer(PUBKEY)).containsExactly(SELF_PAYMENT, SELF_PAYMENT_2); + } + + @Test + void getSelfPaymentsToChannel() { + when(selfPaymentsDao.getSelfPaymentsToChannel(CHANNEL_ID)).thenReturn(List.of(SELF_PAYMENT)); + assertThat(selfPaymentsService.getSelfPaymentsToChannel(CHANNEL_ID)).containsExactly(SELF_PAYMENT); + } + + @Test + void getSelfPaymentsToChannel_closed() { + when(channelService.isClosed(CHANNEL_ID)).thenReturn(true); + when(selfPaymentsDao.getSelfPaymentsToChannel(CHANNEL_ID)).thenReturn(List.of(SELF_PAYMENT)); + assertThat(selfPaymentsService.getSelfPaymentsToChannel(CHANNEL_ID)).containsExactly(SELF_PAYMENT); + } + + @Test + void getSelfPaymentsToPeer() { + when(channelService.getAllChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, CLOSED_CHANNEL_2)); + when(selfPaymentsDao.getSelfPaymentsToChannel(LOCAL_OPEN_CHANNEL.getId())).thenReturn(List.of(SELF_PAYMENT)); + when(selfPaymentsDao.getSelfPaymentsToChannel(CLOSED_CHANNEL_2.getId())).thenReturn(List.of(SELF_PAYMENT_2)); + assertThat(selfPaymentsService.getSelfPaymentsToPeer(PUBKEY)).containsExactly(SELF_PAYMENT, SELF_PAYMENT_2); + } } \ No newline at end of file diff --git a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/SelfPaymentsControllerIT.java b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/SelfPaymentsControllerIT.java index 2609e278..d4cc2d5a 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/SelfPaymentsControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/SelfPaymentsControllerIT.java @@ -15,6 +15,7 @@ import static de.cotto.lndmanagej.SelfPaymentFixtures.SELF_PAYMENT_2; 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_4; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; import static org.hamcrest.core.Is.is; @@ -22,9 +23,11 @@ import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +@SuppressWarnings("PMD.AvoidDuplicateLiterals") @WebMvcTest(controllers = SelfPaymentsController.class) class SelfPaymentsControllerIT { private static final String CHANNEL_PREFIX = "/api/channel/" + CHANNEL_ID.getShortChannelId() + "/"; + private static final String NODE_PREFIX = "/api/node/" + PUBKEY + "/"; @Autowired private MockMvc mockMvc; @@ -45,6 +48,15 @@ class SelfPaymentsControllerIT { .andExpect(jsonPath("$[1].memo", is(SELF_PAYMENT.memo()))); } + @Test + void getSelfPaymentsFromPeer() throws Exception { + when(selfPaymentsService.getSelfPaymentsFromPeer(PUBKEY)) + .thenReturn(List.of(SELF_PAYMENT_2, SELF_PAYMENT)); + mockMvc.perform(get(NODE_PREFIX + "/self-payments-from-peer/")) + .andExpect(jsonPath("$[0].memo", is(SELF_PAYMENT_2.memo()))) + .andExpect(jsonPath("$[1].memo", is(SELF_PAYMENT.memo()))); + } + @Test void getSelfPaymentsToChannel() throws Exception { when(selfPaymentsService.getSelfPaymentsToChannel(CHANNEL_ID)) @@ -64,4 +76,12 @@ class SelfPaymentsControllerIT { .andExpect(jsonPath("$[1].lastChannel", is(CHANNEL_ID.toString()))); } + @Test + void getSelfPaymentsToPeer() throws Exception { + when(selfPaymentsService.getSelfPaymentsToPeer(PUBKEY)) + .thenReturn(List.of(SELF_PAYMENT_2, SELF_PAYMENT)); + mockMvc.perform(get(NODE_PREFIX + "/self-payments-to-peer/")) + .andExpect(jsonPath("$[0].memo", is(SELF_PAYMENT_2.memo()))) + .andExpect(jsonPath("$[1].memo", is(SELF_PAYMENT.memo()))); + } } \ No newline at end of file diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/SelfPaymentsController.java b/web/src/main/java/de/cotto/lndmanagej/controller/SelfPaymentsController.java index d539e1e6..029f8b5c 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/SelfPaymentsController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/SelfPaymentsController.java @@ -4,6 +4,7 @@ import com.codahale.metrics.annotation.Timed; import de.cotto.lndmanagej.controller.dto.ObjectMapperConfiguration; import de.cotto.lndmanagej.controller.dto.SelfPaymentDto; import de.cotto.lndmanagej.model.ChannelId; +import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.service.SelfPaymentsService; import org.springframework.context.annotation.Import; import org.springframework.web.bind.annotation.GetMapping; @@ -23,6 +24,22 @@ public class SelfPaymentsController { this.selfPaymentsService = selfPaymentsService; } + @Timed + @GetMapping("/channel/{channelId}/self-payments-from-channel") + public List getSelfPaymentsFromChannel(@PathVariable ChannelId channelId) { + return selfPaymentsService.getSelfPaymentsFromChannel(channelId).stream() + .map(SelfPaymentDto::createFromModel) + .toList(); + } + + @Timed + @GetMapping("/node/{pubkey}/self-payments-from-peer") + public List getSelfPaymentsFromPeer(@PathVariable Pubkey pubkey) { + return selfPaymentsService.getSelfPaymentsFromPeer(pubkey).stream() + .map(SelfPaymentDto::createFromModel) + .toList(); + } + @Timed @GetMapping("/channel/{channelId}/self-payments-to-channel") public List getSelfPaymentsToChannel(@PathVariable ChannelId channelId) { @@ -32,9 +49,9 @@ public class SelfPaymentsController { } @Timed - @GetMapping("/channel/{channelId}/self-payments-from-channel") - public List getSelfPaymentsFromChannel(@PathVariable ChannelId channelId) { - return selfPaymentsService.getSelfPaymentsFromChannel(channelId).stream() + @GetMapping("/node/{pubkey}/self-payments-to-peer") + public List getSelfPaymentsToPeer(@PathVariable Pubkey pubkey) { + return selfPaymentsService.getSelfPaymentsToPeer(pubkey).stream() .map(SelfPaymentDto::createFromModel) .toList(); } diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/SelfPaymentsControllerTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/SelfPaymentsControllerTest.java index 64f5e1a4..2fea815c 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/SelfPaymentsControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/SelfPaymentsControllerTest.java @@ -13,6 +13,7 @@ import java.util.List; import static de.cotto.lndmanagej.SelfPaymentFixtures.SELF_PAYMENT; import static de.cotto.lndmanagej.SelfPaymentFixtures.SELF_PAYMENT_2; import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -24,6 +25,24 @@ class SelfPaymentsControllerTest { @Mock private SelfPaymentsService service; + @Test + void getSelfPaymentsFromChannel() { + when(service.getSelfPaymentsFromChannel(CHANNEL_ID)).thenReturn(List.of(SELF_PAYMENT, SELF_PAYMENT_2)); + assertThat(selfPaymentsController.getSelfPaymentsFromChannel(CHANNEL_ID)).containsExactly( + SelfPaymentDto.createFromModel(SELF_PAYMENT), + SelfPaymentDto.createFromModel(SELF_PAYMENT_2) + ); + } + + @Test + void getSelfPaymentsFromPeer() { + when(service.getSelfPaymentsFromPeer(PUBKEY)).thenReturn(List.of(SELF_PAYMENT, SELF_PAYMENT_2)); + assertThat(selfPaymentsController.getSelfPaymentsFromPeer(PUBKEY)).containsExactly( + SelfPaymentDto.createFromModel(SELF_PAYMENT), + SelfPaymentDto.createFromModel(SELF_PAYMENT_2) + ); + } + @Test void getSelfPaymentsToChannel() { when(service.getSelfPaymentsToChannel(CHANNEL_ID)).thenReturn(List.of(SELF_PAYMENT, SELF_PAYMENT_2)); @@ -34,11 +53,12 @@ class SelfPaymentsControllerTest { } @Test - void getSelfPaymentsFromChannel() { - when(service.getSelfPaymentsFromChannel(CHANNEL_ID)).thenReturn(List.of(SELF_PAYMENT, SELF_PAYMENT_2)); - assertThat(selfPaymentsController.getSelfPaymentsFromChannel(CHANNEL_ID)).containsExactly( + void getSelfPaymentsToPeer() { + when(service.getSelfPaymentsToPeer(PUBKEY)).thenReturn(List.of(SELF_PAYMENT, SELF_PAYMENT_2)); + assertThat(selfPaymentsController.getSelfPaymentsToPeer(PUBKEY)).containsExactly( SelfPaymentDto.createFromModel(SELF_PAYMENT), SelfPaymentDto.createFromModel(SELF_PAYMENT_2) ); } + } \ No newline at end of file