From df38fdf3e12fb8634ac5dbf7739f7dbadf5f1720 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Fri, 10 Dec 2021 16:23:05 +0100 Subject: [PATCH] compute sweep costs --- .../lndmanagej/service/ChannelService.java | 10 ++ .../service/OnChainCostService.java | 25 +++- .../service/TransactionBackgroundLoader.java | 17 ++- .../service/ChannelServiceTest.java | 13 ++ .../service/OnChainCostServiceTest.java | 140 ++++++++++++++---- .../TransactionBackgroundLoaderTest.java | 14 ++ .../cotto/lndmanagej/model/ClosedChannel.java | 6 + .../lndmanagej/model/CoopClosedChannel.java | 5 + .../lndmanagej/model/ForceClosedChannel.java | 10 ++ .../cotto/lndmanagej/model/OnChainCosts.java | 6 +- .../model/BreachForceClosedChannelTest.java | 10 ++ .../model/CoopClosedChannelTest.java | 13 ++ .../model/ForceClosedChannelTest.java | 10 ++ .../lndmanagej/model/OnChainCostsTest.java | 8 +- .../lndmanagej/model/ResolutionTest.java | 4 +- .../model/OnChainCostsFixtures.java | 3 +- .../lndmanagej/model/ResolutionFixtures.java | 4 +- .../controller/ChannelControllerIT.java | 1 + .../controller/NodeControllerIT.java | 1 + .../controller/OnChainCostsControllerIT.java | 17 ++- .../controller/OnChainCostsController.java | 6 + .../controller/dto/OnChainCostsDto.java | 9 +- .../controller/ChannelControllerTest.java | 3 +- .../controller/NodeControllerTest.java | 3 +- .../OnChainCostsControllerTest.java | 14 ++ .../controller/dto/ChannelDetailsDtoTest.java | 3 +- .../controller/dto/OnChainCostsDtoTest.java | 2 +- 27 files changed, 311 insertions(+), 46 deletions(-) diff --git a/backend/src/main/java/de/cotto/lndmanagej/service/ChannelService.java b/backend/src/main/java/de/cotto/lndmanagej/service/ChannelService.java index de895a58..0e12bfca 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/ChannelService.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/ChannelService.java @@ -7,6 +7,7 @@ import de.cotto.lndmanagej.grpc.GrpcChannels; import de.cotto.lndmanagej.grpc.GrpcClosedChannels; import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.ClosedChannel; +import de.cotto.lndmanagej.model.ForceClosedChannel; import de.cotto.lndmanagej.model.ForceClosingChannel; import de.cotto.lndmanagej.model.LocalChannel; import de.cotto.lndmanagej.model.LocalOpenChannel; @@ -87,6 +88,15 @@ public class ChannelService { .findFirst(); } + @Timed + public Optional getForceClosedChannel(ChannelId channelId) { + return getClosedChannels().stream() + .filter(c -> channelId.equals(c.getId())) + .filter(ClosedChannel::isForceClosed) + .map(ClosedChannel::getAsForceClosedChannel) + .findFirst(); + } + @Timed public Set getForceClosingChannels() { return forceClosingChannelsCache.get(""); diff --git a/backend/src/main/java/de/cotto/lndmanagej/service/OnChainCostService.java b/backend/src/main/java/de/cotto/lndmanagej/service/OnChainCostService.java index 4c3a95fd..7c58cf28 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/OnChainCostService.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/OnChainCostService.java @@ -5,10 +5,12 @@ import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.ChannelPoint; import de.cotto.lndmanagej.model.ClosedChannel; import de.cotto.lndmanagej.model.Coins; +import de.cotto.lndmanagej.model.ForceClosedChannel; import de.cotto.lndmanagej.model.LocalChannel; import de.cotto.lndmanagej.model.OnChainCosts; import de.cotto.lndmanagej.model.OpenInitiator; import de.cotto.lndmanagej.model.Pubkey; +import de.cotto.lndmanagej.model.Resolution; import de.cotto.lndmanagej.transactions.model.Transaction; import de.cotto.lndmanagej.transactions.service.TransactionService; import org.springframework.stereotype.Component; @@ -36,7 +38,8 @@ public class OnChainCostService { public OnChainCosts getOnChainCostsForChannel(LocalChannel localChannel) { return new OnChainCosts( getOpenCostsForChannel(localChannel).orElse(Coins.NONE), - getCloseCostsForChannelId(localChannel.getId()).orElse(Coins.NONE) + getCloseCostsForChannelId(localChannel.getId()).orElse(Coins.NONE), + getSweepCostsForChannelId(localChannel.getId()).orElse(Coins.NONE) ); } @@ -90,6 +93,26 @@ public class OnChainCostService { return Optional.empty(); } + @Timed + public Optional getSweepCostsForChannelId(ChannelId channelId) { + if (channelService.isClosed(channelId)) { + return channelService.getForceClosedChannel(channelId).map(this::getSweepCostsForChannel); + } + return Optional.of(Coins.NONE); + } + + @Timed + public Coins getSweepCostsForChannel(ForceClosedChannel forceClosedChannel) { + return forceClosedChannel.getResolutions().stream() + .map(Resolution::sweepTransaction) + .flatMap(Optional::stream) + .distinct() + .map(transactionService::getTransaction) + .flatMap(Optional::stream) + .map(Transaction::fees) + .reduce(Coins.NONE, Coins::add); + } + private long getNumberOfChannelsWithOpenTransactionHash(String openTransactionHash) { return channelService.getAllLocalChannels() .map(LocalChannel::getChannelPoint) diff --git a/backend/src/main/java/de/cotto/lndmanagej/service/TransactionBackgroundLoader.java b/backend/src/main/java/de/cotto/lndmanagej/service/TransactionBackgroundLoader.java index fbd9352e..db62f368 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/TransactionBackgroundLoader.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/TransactionBackgroundLoader.java @@ -2,12 +2,16 @@ package de.cotto.lndmanagej.service; import de.cotto.lndmanagej.model.Channel; import de.cotto.lndmanagej.model.ChannelPoint; +import de.cotto.lndmanagej.model.ClosedChannel; import de.cotto.lndmanagej.model.ClosedOrClosingChannel; +import de.cotto.lndmanagej.model.ForceClosedChannel; +import de.cotto.lndmanagej.model.Resolution; import de.cotto.lndmanagej.transactions.service.TransactionService; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.Collection; +import java.util.Optional; import java.util.stream.Stream; import static java.util.concurrent.TimeUnit.MINUTES; @@ -33,7 +37,8 @@ public class TransactionBackgroundLoader { private Stream getTransactionHashes() { Stream openTransactionHashes = getOpenTransactionHashes(); Stream closeTransactionHashes = getCloseTransactionHashes(); - return Stream.of(openTransactionHashes, closeTransactionHashes) + Stream sweepTransactionHashes = getSweepTransactionHashes(); + return Stream.of(openTransactionHashes, closeTransactionHashes, sweepTransactionHashes) .flatMap(s -> s); } @@ -54,4 +59,14 @@ public class TransactionBackgroundLoader { .flatMap(Collection::stream) .map(ClosedOrClosingChannel::getCloseTransactionHash); } + + private Stream getSweepTransactionHashes() { + return channelService.getClosedChannels().stream() + .filter(ClosedChannel::isForceClosed) + .map(ClosedChannel::getAsForceClosedChannel) + .map(ForceClosedChannel::getResolutions) + .flatMap(Collection::stream) + .map(Resolution::sweepTransaction) + .flatMap(Optional::stream); + } } diff --git a/backend/src/test/java/de/cotto/lndmanagej/service/ChannelServiceTest.java b/backend/src/test/java/de/cotto/lndmanagej/service/ChannelServiceTest.java index 13b11e2f..d05315ac 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/ChannelServiceTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/ChannelServiceTest.java @@ -17,6 +17,7 @@ import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL_2; import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL_3; import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL_TO_NODE_3; +import static de.cotto.lndmanagej.model.ForceClosedChannelFixtures.FORCE_CLOSED_CHANNEL_2; import static de.cotto.lndmanagej.model.ForceClosingChannelFixtures.FORCE_CLOSING_CHANNEL; import static de.cotto.lndmanagej.model.ForceClosingChannelFixtures.FORCE_CLOSING_CHANNEL_2; import static de.cotto.lndmanagej.model.ForceClosingChannelFixtures.FORCE_CLOSING_CHANNEL_TO_NODE_3; @@ -183,6 +184,18 @@ class ChannelServiceTest { assertThat(channelService.getClosedChannel(CHANNEL_ID_2)).isEmpty(); } + @Test + void getForceClosedChannel() { + when(grpcClosedChannels.getClosedChannels()).thenReturn(Set.of(FORCE_CLOSED_CHANNEL_2, CLOSED_CHANNEL_2)); + assertThat(channelService.getForceClosedChannel(CHANNEL_ID_2)).contains(FORCE_CLOSED_CHANNEL_2); + } + + @Test + void getForceClosedChannel_not_force_closed() { + when(grpcClosedChannels.getClosedChannels()).thenReturn(Set.of(CLOSED_CHANNEL_2)); + assertThat(channelService.getForceClosedChannel(CHANNEL_ID_2)).isEmpty(); + } + @Test void getForceClosingChannels() { when(grpcChannels.getForceClosingChannels()) diff --git a/backend/src/test/java/de/cotto/lndmanagej/service/OnChainCostServiceTest.java b/backend/src/test/java/de/cotto/lndmanagej/service/OnChainCostServiceTest.java index b8a25bea..69f76dc9 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/OnChainCostServiceTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/OnChainCostServiceTest.java @@ -1,5 +1,6 @@ package de.cotto.lndmanagej.service; +import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.ClosedChannel; import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.LocalChannel; @@ -18,9 +19,11 @@ import java.util.Set; import java.util.stream.Stream; import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; +import static de.cotto.lndmanagej.model.ChannelPointFixtures.TRANSACTION_HASH_3; import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL; import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL_2; import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL_3; +import static de.cotto.lndmanagej.model.ForceClosedChannelFixtures.FORCE_CLOSED_CHANNEL; import static de.cotto.lndmanagej.model.ForceClosedChannelFixtures.FORCE_CLOSED_CHANNEL_2; import static de.cotto.lndmanagej.model.ForceClosedChannelFixtures.FORCE_CLOSED_CHANNEL_OPEN_LOCAL; import static de.cotto.lndmanagej.model.ForceClosedChannelFixtures.FORCE_CLOSED_CHANNEL_OPEN_REMOTE; @@ -37,6 +40,7 @@ import static de.cotto.lndmanagej.transactions.model.TransactionFixtures.FEES_2; import static de.cotto.lndmanagej.transactions.model.TransactionFixtures.TRANSACTION; import static de.cotto.lndmanagej.transactions.model.TransactionFixtures.TRANSACTION_2; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; @@ -64,11 +68,25 @@ class OnChainCostServiceTest { .thenReturn(Optional.of(FORCE_CLOSED_CHANNEL_OPEN_LOCAL)); OnChainCosts expected = new OnChainCosts( FEES, - FEES_2 + FEES_2, + Coins.NONE ); assertThat(onChainCostService.getOnChainCostsForChannelId(CHANNEL_ID)).isEqualTo(expected); } + @Test + void getOnChainCostsForChannelId_sweep_costs() { + ChannelId channelId = mockForSweepCosts(); + when(channelService.getLocalChannel(channelId)).thenReturn(Optional.of(FORCE_CLOSED_CHANNEL)); + + OnChainCosts expected = new OnChainCosts( + Coins.NONE, + Coins.NONE, + TRANSACTION.fees() + ); + assertThat(onChainCostService.getOnChainCostsForChannelId(channelId)).isEqualTo(expected); + } + @Test void getOnChainCostsForChannel() { mockOpenTransaction(FORCE_CLOSED_CHANNEL_OPEN_LOCAL); @@ -78,11 +96,23 @@ class OnChainCostServiceTest { .thenReturn(Optional.of(FORCE_CLOSED_CHANNEL_OPEN_LOCAL)); OnChainCosts expected = new OnChainCosts( FEES, - FEES_2 + FEES_2, + Coins.NONE ); assertThat(onChainCostService.getOnChainCostsForChannel(FORCE_CLOSED_CHANNEL_OPEN_LOCAL)).isEqualTo(expected); } + @Test + void getOnChainCostsForChannel_sweep_costs() { + mockForSweepCosts(); + OnChainCosts expected = new OnChainCosts( + Coins.NONE, + Coins.NONE, + TRANSACTION.fees() + ); + assertThat(onChainCostService.getOnChainCostsForChannel(FORCE_CLOSED_CHANNEL)).isEqualTo(expected); + } + @Test void getOnChainCostsForPeer() { when(channelService.getAllLocalChannels()) @@ -100,7 +130,8 @@ class OnChainCostServiceTest { .thenReturn(Set.of(LOCAL_OPEN_CHANNEL_TO_NODE_3, FORCE_CLOSED_CHANNEL_2)); OnChainCosts expected = new OnChainCosts( FEES.add(FEES_2), - FEES + FEES, + Coins.NONE ); assertThat(onChainCostService.getOnChainCostsForPeer(PUBKEY)).isEqualTo(expected); } @@ -108,99 +139,99 @@ class OnChainCostServiceTest { @Nested class GetOpenCosts { @Test - void getOpenCosts_by_channel_id_not_resolved() { + void by_channel_id_not_resolved() { assertThat(onChainCostService.getOpenCostsForChannelId(CHANNEL_ID)).isEmpty(); } @Test - void getOpenCosts_by_channel_id_resolved() { + void by_channel_id_resolved() { mockOpenTransaction(LOCAL_OPEN_CHANNEL); when(channelService.getLocalChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL)); assertThat(onChainCostService.getOpenCostsForChannelId(CHANNEL_ID)).contains(OPEN_COSTS); } @Test - void getOpenCosts_for_local_open_channel_initiator_local_transaction_not_found() { + void for_local_open_channel_initiator_local_transaction_not_found() { assertThat(onChainCostService.getOpenCostsForChannel(LOCAL_OPEN_CHANNEL)).isEmpty(); } @Test - void getOpenCosts_for_local_open_channel_initiator_local() { + void for_local_open_channel_initiator_local() { mockOpenTransaction(LOCAL_OPEN_CHANNEL); assertThat(onChainCostService.getOpenCostsForChannel(LOCAL_OPEN_CHANNEL)) .contains(OPEN_COSTS); } @Test - void getOpenCosts_for_local_open_channel_initiator_remote() { + void for_local_open_channel_initiator_remote() { mockOpenTransaction(LOCAL_OPEN_CHANNEL_2); assertThat(onChainCostService.getOpenCostsForChannel(LOCAL_OPEN_CHANNEL_2)) .contains(Coins.NONE); } @Test - void getOpenCosts_for_coop_closed_channel_initiator_local() { + void for_coop_closed_channel_initiator_local() { mockOpenTransaction(CLOSED_CHANNEL); assertThat(onChainCostService.getOpenCostsForChannel(CLOSED_CHANNEL)) .contains(OPEN_COSTS); } @Test - void getOpenCosts_for_coop_closed_channel_initiator_unknown() { + void for_coop_closed_channel_initiator_unknown() { assertThat(onChainCostService.getOpenCostsForChannel(CLOSED_CHANNEL_2)).isEmpty(); } @Test - void getOpenCosts_for_coop_closed_channel_initiator_remote() { + void for_coop_closed_channel_initiator_remote() { mockOpenTransaction(CLOSED_CHANNEL_3); assertThat(onChainCostService.getOpenCostsForChannel(CLOSED_CHANNEL_3)) .contains(Coins.NONE); } @Test - void getOpenCosts_for_force_closed_channel_initiator_local() { + void for_force_closed_channel_initiator_local() { mockOpenTransaction(FORCE_CLOSED_CHANNEL_OPEN_LOCAL); assertThat(onChainCostService.getOpenCostsForChannel(FORCE_CLOSED_CHANNEL_OPEN_LOCAL)) .contains(OPEN_COSTS); } @Test - void getOpenCosts_for_force_closed_channel_initiator_remote() { + void for_force_closed_channel_initiator_remote() { mockOpenTransaction(FORCE_CLOSED_CHANNEL_OPEN_REMOTE); assertThat(onChainCostService.getOpenCostsForChannel(FORCE_CLOSED_CHANNEL_OPEN_REMOTE)) .contains(Coins.NONE); } @Test - void getOpenCosts_for_waiting_close_channel_initiator_local() { + void for_waiting_close_channel_initiator_local() { mockOpenTransaction(WAITING_CLOSE_CHANNEL); assertThat(onChainCostService.getOpenCostsForChannel(WAITING_CLOSE_CHANNEL)) .contains(OPEN_COSTS); } @Test - void getOpenCosts_for_waiting_close_channel_initiator_remote() { + void for_waiting_close_channel_initiator_remote() { mockOpenTransaction(WAITING_CLOSE_CHANNEL_2); assertThat(onChainCostService.getOpenCostsForChannel(WAITING_CLOSE_CHANNEL_2)) .contains(Coins.NONE); } @Test - void getOpenCosts_for_force_closing_channel_initiator_local() { + void for_force_closing_channel_initiator_local() { mockOpenTransaction(FORCE_CLOSING_CHANNEL); assertThat(onChainCostService.getOpenCostsForChannel(FORCE_CLOSING_CHANNEL)) .contains(OPEN_COSTS); } @Test - void getOpenCosts_for_force_closing_channel_initiator_remote() { + void for_force_closing_channel_initiator_remote() { mockOpenTransaction(FORCE_CLOSING_CHANNEL_2); assertThat(onChainCostService.getOpenCostsForChannel(FORCE_CLOSING_CHANNEL_2)) .contains(Coins.NONE); } @Test - void getOpenCosts_for_transaction_opening_several_channels_divisible() { + void for_transaction_opening_several_channels_divisible() { assertThat(OPEN_COSTS.satoshis()).isEqualTo(124); mockOpenTransaction(LOCAL_OPEN_CHANNEL); when(channelService.getAllLocalChannels()).thenReturn(Stream.of(LOCAL_OPEN_CHANNEL, LOCAL_OPEN_CHANNEL_2)); @@ -209,7 +240,7 @@ class OnChainCostServiceTest { } @Test - void getOpenCosts_for_transaction_opening_several_channels_not_divisible() { + void for_transaction_opening_several_channels_not_divisible() { assertThat(OPEN_COSTS.satoshis()).isEqualTo(124); mockOpenTransaction(LOCAL_OPEN_CHANNEL); when(channelService.getAllLocalChannels()).thenReturn( @@ -230,62 +261,106 @@ class OnChainCostServiceTest { } @Test - void getCloseCosts_by_channel_id_not_resolved() { + void by_channel_id_not_resolved() { assertThat(onChainCostService.getCloseCostsForChannelId(CHANNEL_ID)).isEmpty(); } @Test - void getCloseCosts_by_channel_id_not_closed() { + void by_channel_id_not_closed() { when(channelService.isClosed(CHANNEL_ID)).thenReturn(false); assertThat(onChainCostService.getCloseCostsForChannelId(CHANNEL_ID)).contains(Coins.NONE); } @Test - void getCloseCosts_by_channel_id_resolved() { + void by_channel_id_resolved() { mockCloseTransaction(CLOSED_CHANNEL); when(channelService.getClosedChannel(CHANNEL_ID)).thenReturn(Optional.of(CLOSED_CHANNEL)); assertThat(onChainCostService.getCloseCostsForChannelId(CHANNEL_ID)).contains(CLOSE_COSTS); } @Test - void getOpenCosts_for_coop_closed_channel_initiator_local_transaction_not_found() { + void for_coop_closed_channel_initiator_local_transaction_not_found() { assertThat(onChainCostService.getCloseCostsForChannel(CLOSED_CHANNEL)).isEmpty(); } @Test - void getOpenCosts_for_coop_closed_channel_initiator_local() { + void for_coop_closed_channel_initiator_local() { mockCloseTransaction(CLOSED_CHANNEL); assertThat(onChainCostService.getCloseCostsForChannel(CLOSED_CHANNEL)) .contains(CLOSE_COSTS); } @Test - void getOpenCosts_for_coop_closed_channel_initiator_unknown() { + void for_coop_closed_channel_initiator_unknown() { assertThat(onChainCostService.getCloseCostsForChannel(CLOSED_CHANNEL_2)).isEmpty(); } @Test - void getOpenCosts_for_coop_closed_channel_initiator_remote() { + void for_coop_closed_channel_initiator_remote() { mockCloseTransaction(CLOSED_CHANNEL_3); assertThat(onChainCostService.getCloseCostsForChannel(CLOSED_CHANNEL_3)) .contains(Coins.NONE); } @Test - void getOpenCosts_for_force_closed_channel_initiator_local() { + void for_force_closed_channel_initiator_local() { mockCloseTransaction(FORCE_CLOSED_CHANNEL_OPEN_LOCAL); assertThat(onChainCostService.getCloseCostsForChannel(FORCE_CLOSED_CHANNEL_OPEN_LOCAL)) .contains(CLOSE_COSTS); } @Test - void getOpenCosts_for_force_closed_channel_initiator_remote() { + void for_force_closed_channel_initiator_remote() { mockCloseTransaction(FORCE_CLOSED_CHANNEL_OPEN_REMOTE); assertThat(onChainCostService.getCloseCostsForChannel(FORCE_CLOSED_CHANNEL_OPEN_REMOTE)) .contains(Coins.NONE); } } + @Nested + class GetSweepCosts { + @BeforeEach + void setUp() { + lenient().when(channelService.isClosed(CHANNEL_ID)).thenReturn(true); + } + + @Test + void by_channel_id_not_resolved() { + assertThat(onChainCostService.getSweepCostsForChannelId(CHANNEL_ID)).isEmpty(); + } + + @Test + void by_channel_id_not_closed() { + when(channelService.isClosed(CHANNEL_ID)).thenReturn(false); + assertThat(onChainCostService.getSweepCostsForChannelId(CHANNEL_ID)).contains(Coins.NONE); + } + + @Test + void by_channel_id_resolved() { + when(transactionService.getTransaction(TRANSACTION_HASH_3)).thenReturn(Optional.of(TRANSACTION_2)); + when(channelService.getForceClosedChannel(CHANNEL_ID)).thenReturn(Optional.of(FORCE_CLOSED_CHANNEL)); + assertThat(onChainCostService.getSweepCostsForChannelId(CHANNEL_ID)).contains(TRANSACTION_2.fees()); + } + + @Test + void transaction_not_found() { + when(transactionService.getTransaction(TRANSACTION_HASH_3)).thenReturn(Optional.empty()); + assertThat(onChainCostService.getSweepCostsForChannel(FORCE_CLOSED_CHANNEL)).isEqualTo(Coins.NONE); + } + + @Test + void no_sweep_transaction() { + assertThat(onChainCostService.getSweepCostsForChannel(FORCE_CLOSED_CHANNEL_2)).isEqualTo(Coins.NONE); + } + + @Test + void with_sweep_transaction() { + when(transactionService.getTransaction(TRANSACTION_HASH_3)).thenReturn(Optional.of(TRANSACTION_2)); + assertThat(onChainCostService.getSweepCostsForChannel(FORCE_CLOSED_CHANNEL)) + .isEqualTo(TRANSACTION_2.fees()); + } + } + private void mockOpenTransaction(LocalChannel channel) { lenient().when(channelService.getAllLocalChannels()).thenReturn(Stream.of(channel)); lenient().when(transactionService.getTransaction(channel.getChannelPoint().getTransactionHash())) @@ -296,4 +371,13 @@ class OnChainCostServiceTest { lenient().when(transactionService.getTransaction(channel.getCloseTransactionHash())) .thenReturn(Optional.of(TRANSACTION_2)); } + + private ChannelId mockForSweepCosts() { + ChannelId channelId = FORCE_CLOSED_CHANNEL.getId(); + when(transactionService.getTransaction(any())).thenReturn(Optional.empty()); + when(transactionService.getTransaction(TRANSACTION_HASH_3)).thenReturn(Optional.of(TRANSACTION)); + when(channelService.isClosed(channelId)).thenReturn(true); + when(channelService.getForceClosedChannel(channelId)).thenReturn(Optional.of(FORCE_CLOSED_CHANNEL)); + return channelId; + } } \ No newline at end of file diff --git a/backend/src/test/java/de/cotto/lndmanagej/service/TransactionBackgroundLoaderTest.java b/backend/src/test/java/de/cotto/lndmanagej/service/TransactionBackgroundLoaderTest.java index 17669edd..76d942ea 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/TransactionBackgroundLoaderTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/TransactionBackgroundLoaderTest.java @@ -20,7 +20,11 @@ import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3; import static de.cotto.lndmanagej.model.ChannelPointFixtures.CHANNEL_POINT; import static de.cotto.lndmanagej.model.ChannelPointFixtures.CHANNEL_POINT_2; import static de.cotto.lndmanagej.model.ChannelPointFixtures.CHANNEL_POINT_3; +import static de.cotto.lndmanagej.model.ChannelPointFixtures.TRANSACTION_HASH; +import static de.cotto.lndmanagej.model.ChannelPointFixtures.TRANSACTION_HASH_2; +import static de.cotto.lndmanagej.model.ChannelPointFixtures.TRANSACTION_HASH_3; import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL; +import static de.cotto.lndmanagej.model.ForceClosedChannelFixtures.FORCE_CLOSED_CHANNEL; import static de.cotto.lndmanagej.model.ForceClosingChannelFixtures.FORCE_CLOSING_CHANNEL; import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL; import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_2; @@ -136,6 +140,16 @@ class TransactionBackgroundLoaderTest { verify(transactionService, never()).getTransaction(htlcOutpointHash); } + @Test + void update_from_force_closed_channels_sweep_transaction() { + when(transactionService.isUnknown(TRANSACTION_HASH)).thenReturn(false); + when(transactionService.isUnknown(TRANSACTION_HASH_2)).thenReturn(false); + when(transactionService.isUnknown(TRANSACTION_HASH_3)).thenReturn(true); + when(channelService.getClosedChannels()).thenReturn(Set.of(FORCE_CLOSED_CHANNEL)); + transactionBackgroundLoader.loadTransactionForOneChannel(); + verify(transactionService).getTransaction(TRANSACTION_HASH_3); + } + @Test void update_one_unknown() { LocalOpenChannel channel1 = new LocalOpenChannel( diff --git a/model/src/main/java/de/cotto/lndmanagej/model/ClosedChannel.java b/model/src/main/java/de/cotto/lndmanagej/model/ClosedChannel.java index f9ce2d50..1fb95d9b 100644 --- a/model/src/main/java/de/cotto/lndmanagej/model/ClosedChannel.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/ClosedChannel.java @@ -31,6 +31,8 @@ public abstract class ClosedChannel extends ClosedOrClosingChannel { this.closeHeight = closeHeight; } + public abstract boolean isForceClosed(); + public CloseInitiator getCloseInitiator() { return closeInitiator; } @@ -44,6 +46,10 @@ public abstract class ClosedChannel extends ClosedOrClosingChannel { return this; } + public ForceClosedChannel getAsForceClosedChannel() { + throw new IllegalStateException("Channel is not force-closed"); + } + @Override public ChannelStatus getStatus() { return new ChannelStatus(isPrivateChannel(), false, true, CLOSED); diff --git a/model/src/main/java/de/cotto/lndmanagej/model/CoopClosedChannel.java b/model/src/main/java/de/cotto/lndmanagej/model/CoopClosedChannel.java index 143a5ffa..f496b834 100644 --- a/model/src/main/java/de/cotto/lndmanagej/model/CoopClosedChannel.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/CoopClosedChannel.java @@ -20,4 +20,9 @@ public class CoopClosedChannel extends ClosedChannel { closeHeight ); } + + @Override + public boolean isForceClosed() { + return false; + } } diff --git a/model/src/main/java/de/cotto/lndmanagej/model/ForceClosedChannel.java b/model/src/main/java/de/cotto/lndmanagej/model/ForceClosedChannel.java index d69bd29a..660b6470 100644 --- a/model/src/main/java/de/cotto/lndmanagej/model/ForceClosedChannel.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/ForceClosedChannel.java @@ -32,6 +32,16 @@ public class ForceClosedChannel extends ClosedChannel { return resolutions; } + @Override + public boolean isForceClosed() { + return true; + } + + @Override + public ForceClosedChannel getAsForceClosedChannel() { + return this; + } + @Override public boolean equals(Object other) { if (this == other) { diff --git a/model/src/main/java/de/cotto/lndmanagej/model/OnChainCosts.java b/model/src/main/java/de/cotto/lndmanagej/model/OnChainCosts.java index c2393955..67e2ee0f 100644 --- a/model/src/main/java/de/cotto/lndmanagej/model/OnChainCosts.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/OnChainCosts.java @@ -1,9 +1,9 @@ package de.cotto.lndmanagej.model; -public record OnChainCosts(Coins open, Coins close) { - public static final OnChainCosts NONE = new OnChainCosts(Coins.NONE, Coins.NONE); +public record OnChainCosts(Coins open, Coins close, Coins sweep) { + public static final OnChainCosts NONE = new OnChainCosts(Coins.NONE, Coins.NONE, Coins.NONE); public OnChainCosts add(OnChainCosts other) { - return new OnChainCosts(open.add(other.open), close.add(other.close)); + return new OnChainCosts(open.add(other.open), close.add(other.close), sweep.add(other.sweep)); } } diff --git a/model/src/test/java/de/cotto/lndmanagej/model/BreachForceClosedChannelTest.java b/model/src/test/java/de/cotto/lndmanagej/model/BreachForceClosedChannelTest.java index 4bc3b9b9..d5e14093 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/BreachForceClosedChannelTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/BreachForceClosedChannelTest.java @@ -69,6 +69,11 @@ class BreachForceClosedChannelTest { assertThat(FORCE_CLOSED_CHANNEL_BREACH.getOpenInitiator()).isEqualTo(OpenInitiator.LOCAL); } + @Test + void isForceClosed() { + assertThat(FORCE_CLOSED_CHANNEL_BREACH.isForceClosed()).isTrue(); + } + @Test void getCloseInitiator() { assertThat(FORCE_CLOSED_CHANNEL_BREACH.getCloseInitiator()).isEqualTo(CloseInitiator.REMOTE); @@ -95,6 +100,11 @@ class BreachForceClosedChannelTest { assertThat(FORCE_CLOSED_CHANNEL_BREACH.getAsClosedChannel()).isEqualTo(FORCE_CLOSED_CHANNEL_BREACH); } + @Test + void getAsForceClosedChannel() { + assertThat(FORCE_CLOSED_CHANNEL_BREACH.getAsForceClosedChannel()).isEqualTo(FORCE_CLOSED_CHANNEL_BREACH); + } + @Test void getResolutions() { assertThat(FORCE_CLOSED_CHANNEL_BREACH.getResolutions()).containsExactlyInAnyOrder(RESOLUTION_2); diff --git a/model/src/test/java/de/cotto/lndmanagej/model/CoopClosedChannelTest.java b/model/src/test/java/de/cotto/lndmanagej/model/CoopClosedChannelTest.java index cb3e538a..8d4ff9c6 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/CoopClosedChannelTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/CoopClosedChannelTest.java @@ -15,6 +15,7 @@ import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; class CoopClosedChannelTest { @Test @@ -99,6 +100,18 @@ class CoopClosedChannelTest { assertThat(CLOSED_CHANNEL.getCloseInitiator()).isEqualTo(CloseInitiator.REMOTE); } + @Test + void isForceClosed() { + assertThat(CLOSED_CHANNEL.isForceClosed()).isFalse(); + } + + @Test + void getAsForceClosedChannel() { + assertThatIllegalStateException() + .isThrownBy(CLOSED_CHANNEL::getAsForceClosedChannel) + .withMessage("Channel is not force-closed"); + } + @Test void getCloseHeight() { assertThat(CLOSED_CHANNEL.getCloseHeight()).isEqualTo(987_654); diff --git a/model/src/test/java/de/cotto/lndmanagej/model/ForceClosedChannelTest.java b/model/src/test/java/de/cotto/lndmanagej/model/ForceClosedChannelTest.java index b26e7df4..588410da 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/ForceClosedChannelTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/ForceClosedChannelTest.java @@ -87,6 +87,16 @@ class ForceClosedChannelTest { assertThat(FORCE_CLOSED_CHANNEL_REMOTE.getCloseInitiator()).isEqualTo(CloseInitiator.REMOTE); } + @Test + void isForceClosed() { + assertThat(FORCE_CLOSED_CHANNEL_REMOTE.isForceClosed()).isTrue(); + } + + @Test + void getAsForceClosedChannel() { + assertThat(FORCE_CLOSED_CHANNEL_REMOTE.getAsForceClosedChannel()).isEqualTo(FORCE_CLOSED_CHANNEL_REMOTE); + } + @Test void getCloseHeight() { assertThat(FORCE_CLOSED_CHANNEL_REMOTE.getCloseHeight()).isEqualTo(987_654); diff --git a/model/src/test/java/de/cotto/lndmanagej/model/OnChainCostsTest.java b/model/src/test/java/de/cotto/lndmanagej/model/OnChainCostsTest.java index 00744e76..e103f3bc 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/OnChainCostsTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/OnChainCostsTest.java @@ -15,7 +15,8 @@ class OnChainCostsTest { void add() { assertThat(ON_CHAIN_COSTS.add(ON_CHAIN_COSTS)).isEqualTo(new OnChainCosts( Coins.ofSatoshis(2000), - Coins.ofSatoshis(4000) + Coins.ofSatoshis(4000), + Coins.ofSatoshis(6000) )); } @@ -28,4 +29,9 @@ class OnChainCostsTest { void close() { assertThat(ON_CHAIN_COSTS.close()).isEqualTo(Coins.ofSatoshis(2000)); } + + @Test + void sweep() { + assertThat(ON_CHAIN_COSTS.sweep()).isEqualTo(Coins.ofSatoshis(3000)); + } } \ No newline at end of file diff --git a/model/src/test/java/de/cotto/lndmanagej/model/ResolutionTest.java b/model/src/test/java/de/cotto/lndmanagej/model/ResolutionTest.java index 57e63414..a15e2010 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/ResolutionTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/ResolutionTest.java @@ -2,7 +2,7 @@ package de.cotto.lndmanagej.model; import org.junit.jupiter.api.Test; -import static de.cotto.lndmanagej.model.ChannelPointFixtures.TRANSACTION_HASH; +import static de.cotto.lndmanagej.model.ChannelPointFixtures.TRANSACTION_HASH_3; import static de.cotto.lndmanagej.model.ResolutionFixtures.RESOLUTION; import static de.cotto.lndmanagej.model.ResolutionFixtures.RESOLUTION_2; import static org.assertj.core.api.Assertions.assertThat; @@ -16,6 +16,6 @@ class ResolutionTest { @Test void sweepTransaction() { - assertThat(RESOLUTION.sweepTransaction()).contains(TRANSACTION_HASH); + assertThat(RESOLUTION.sweepTransaction()).contains(TRANSACTION_HASH_3); } } \ No newline at end of file diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnChainCostsFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnChainCostsFixtures.java index 1a846413..d3e403a0 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnChainCostsFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnChainCostsFixtures.java @@ -3,6 +3,7 @@ package de.cotto.lndmanagej.model; public class OnChainCostsFixtures { public static final OnChainCosts ON_CHAIN_COSTS = new OnChainCosts( Coins.ofSatoshis(1000), - Coins.ofSatoshis(2000) + Coins.ofSatoshis(2000), + Coins.ofSatoshis(3000) ); } diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ResolutionFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ResolutionFixtures.java index fdb74390..797bfc8c 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ResolutionFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ResolutionFixtures.java @@ -2,9 +2,9 @@ package de.cotto.lndmanagej.model; import java.util.Optional; -import static de.cotto.lndmanagej.model.ChannelPointFixtures.TRANSACTION_HASH; +import static de.cotto.lndmanagej.model.ChannelPointFixtures.TRANSACTION_HASH_3; public class ResolutionFixtures { - public static final Resolution RESOLUTION = new Resolution(Optional.of(TRANSACTION_HASH)); + public static final Resolution RESOLUTION = new Resolution(Optional.of(TRANSACTION_HASH_3)); public static final Resolution RESOLUTION_2 = new Resolution(Optional.empty()); } 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 5fe524d7..f90db463 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/ChannelControllerIT.java @@ -149,6 +149,7 @@ class ChannelControllerIT { .andExpect(jsonPath("$.status.openClosed", is("OPEN"))) .andExpect(jsonPath("$.onChainCosts.openCosts", is("1000"))) .andExpect(jsonPath("$.onChainCosts.closeCosts", is("2000"))) + .andExpect(jsonPath("$.onChainCosts.sweepCosts", is("3000"))) .andExpect(jsonPath("$.offChainCosts.rebalanceSource", is("1"))) .andExpect(jsonPath("$.offChainCosts.rebalanceTarget", is("2"))) .andExpect(jsonPath("$.balance.localBalance", is("2000"))) 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 ded86729..323053fa 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java @@ -111,6 +111,7 @@ class NodeControllerIT { .andExpect(jsonPath("$.feeReport.sourced", is("567"))) .andExpect(jsonPath("$.onChainCosts.openCosts", is("1000"))) .andExpect(jsonPath("$.onChainCosts.closeCosts", is("2000"))) + .andExpect(jsonPath("$.onChainCosts.sweepCosts", is("3000"))) .andExpect(jsonPath("$.online", is(true))); } diff --git a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/OnChainCostsControllerIT.java b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/OnChainCostsControllerIT.java index fdba396f..51b6f7b9 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/OnChainCostsControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/OnChainCostsControllerIT.java @@ -41,7 +41,8 @@ class OnChainCostsControllerIT { when(onChainCostService.getOnChainCostsForPeer(PUBKEY)).thenReturn(ON_CHAIN_COSTS); mockMvc.perform(get(PEER_PREFIX + "/on-chain-costs")) .andExpect(jsonPath("$.openCosts", is("1000"))) - .andExpect(jsonPath("$.closeCosts", is("2000"))); + .andExpect(jsonPath("$.closeCosts", is("2000"))) + .andExpect(jsonPath("$.sweepCosts", is("3000"))); } @Test @@ -65,10 +66,24 @@ class OnChainCostsControllerIT { .andExpect(content().string("123")); } + @Test + void sweep_costs_for_channel() throws Exception { + when(onChainCostService.getSweepCostsForChannelId(CHANNEL_ID)).thenReturn(Optional.of(Coins.ofSatoshis(123))); + mockMvc.perform(get(CHANNEL_PREFIX + "/sweep-costs")) + .andExpect(content().string("123")); + } + @Test void close_costs_for_channel_unknown() throws Exception { mockMvc.perform(get(CHANNEL_PREFIX + "/close-costs")) .andExpect(status().isBadRequest()) .andExpect(content().string("Unable to get close costs for channel with ID " + CHANNEL_ID)); } + + @Test + void sweep_costs_channel_unknown() throws Exception { + mockMvc.perform(get(CHANNEL_PREFIX + "/sweep-costs")) + .andExpect(status().isBadRequest()) + .andExpect(content().string("Unable to get sweep costs for channel with ID " + CHANNEL_ID)); + } } \ No newline at end of file diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/OnChainCostsController.java b/web/src/main/java/de/cotto/lndmanagej/controller/OnChainCostsController.java index 74211f78..598f7beb 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/OnChainCostsController.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/OnChainCostsController.java @@ -40,4 +40,10 @@ public class OnChainCostsController { .orElseThrow(() -> new CostException("Unable to get close costs for channel with ID " + channelId)); } + @Timed + @GetMapping("/channel/{channelId}/sweep-costs") + public long getSweepCostsForChannel(@PathVariable ChannelId channelId) throws CostException { + return onChainCostService.getSweepCostsForChannelId(channelId).map(Coins::satoshis) + .orElseThrow(() -> new CostException("Unable to get sweep costs for channel with ID " + channelId)); + } } diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnChainCostsDto.java b/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnChainCostsDto.java index 6cf3f484..1c8cd733 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnChainCostsDto.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnChainCostsDto.java @@ -2,10 +2,15 @@ package de.cotto.lndmanagej.controller.dto; import de.cotto.lndmanagej.model.OnChainCosts; -public record OnChainCostsDto(String openCosts, String closeCosts) { +public record OnChainCostsDto(String openCosts, String closeCosts, String sweepCosts) { public static OnChainCostsDto createFromModel(OnChainCosts onChainCosts) { long openSatoshi = onChainCosts.open().satoshis(); long closeSatoshi = onChainCosts.close().satoshis(); - return new OnChainCostsDto(String.valueOf(openSatoshi), String.valueOf(closeSatoshi)); + long sweepSatoshi = onChainCosts.sweep().satoshis(); + return new OnChainCostsDto( + String.valueOf(openSatoshi), + String.valueOf(closeSatoshi), + String.valueOf(sweepSatoshi) + ); } } 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 b3272b5f..7e5b745a 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/ChannelControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/ChannelControllerTest.java @@ -47,9 +47,10 @@ import static org.mockito.Mockito.when; class ChannelControllerTest { private static final Coins OPEN_COSTS = Coins.ofSatoshis(1); private static final Coins CLOSE_COSTS = Coins.ofSatoshis(2); + private static final Coins SWEEP_COSTS = Coins.ofSatoshis(3); private static final Coins SOURCE_COSTS = Coins.ofSatoshis(3); private static final Coins TARGET_COSTS = Coins.ofSatoshis(4); - private static final OnChainCosts ON_CHAIN_COSTS = new OnChainCosts(OPEN_COSTS, CLOSE_COSTS); + private static final OnChainCosts ON_CHAIN_COSTS = new OnChainCosts(OPEN_COSTS, CLOSE_COSTS, SWEEP_COSTS); private static final OffChainCostsDto OFF_CHAIN_COSTS = new OffChainCostsDto(SOURCE_COSTS, TARGET_COSTS); private static final PoliciesDto FEE_CONFIGURATION_DTO = PoliciesDto.createFromModel(POLICIES); private static final ClosedChannelDetailsDto CLOSED_CHANNEL_DETAILS_DTO = 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 56287b49..88cb9bfb 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/NodeControllerTest.java @@ -118,7 +118,8 @@ class NodeControllerTest { ); OnChainCosts onChainCosts = new OnChainCosts( Coins.ofSatoshis(123), - Coins.ofSatoshis(456) + Coins.ofSatoshis(456), + Coins.ofSatoshis(789) ); Coins rebalanceSourceCosts = Coins.ofMilliSatoshis(1); Coins rebalanceTargetCosts = Coins.ofMilliSatoshis(2); diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/OnChainCostsControllerTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/OnChainCostsControllerTest.java index 9ed832c2..fe119490 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/OnChainCostsControllerTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/OnChainCostsControllerTest.java @@ -60,4 +60,18 @@ class OnChainCostsControllerTest { () -> onChainCostsController.getCloseCostsForChannel(CHANNEL_ID) ); } + + @Test + void getSweepCostsForChannel() throws CostException { + Coins coins = Coins.ofSatoshis(123); + when(onChainCostService.getSweepCostsForChannelId(CHANNEL_ID)).thenReturn(Optional.of(coins)); + assertThat(onChainCostsController.getSweepCostsForChannel(CHANNEL_ID)).isEqualTo(coins.satoshis()); + } + + @Test + void getSweepCostsForChannel_unknown() { + assertThatExceptionOfType(CostException.class).isThrownBy( + () -> onChainCostsController.getSweepCostsForChannel(CHANNEL_ID) + ); + } } \ 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 287845be..50fbf33a 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 @@ -20,7 +20,8 @@ import static org.assertj.core.api.Assertions.assertThat; class ChannelDetailsDtoTest { - private static final OnChainCosts ON_CHAIN_COSTS = new OnChainCosts(Coins.ofSatoshis(1), Coins.ofSatoshis(2)); + private static final OnChainCosts ON_CHAIN_COSTS = + new OnChainCosts(Coins.ofSatoshis(1), Coins.ofSatoshis(2), Coins.ofSatoshis(3)); private static final OffChainCostsDto OFF_CHAIN_COSTS = new OffChainCostsDto(Coins.ofSatoshis(3), Coins.ofSatoshis(4)); private static final ClosedChannelDetailsDto CLOSE_DETAILS = new ClosedChannelDetailsDto("abc", 123); diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnChainCostsDtoTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnChainCostsDtoTest.java index 999defd6..f660d5d9 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnChainCostsDtoTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnChainCostsDtoTest.java @@ -9,6 +9,6 @@ class OnChainCostsDtoTest { @Test void createFromModel() { assertThat(OnChainCostsDto.createFromModel(ON_CHAIN_COSTS)) - .isEqualTo(new OnChainCostsDto("1000", "2000")); + .isEqualTo(new OnChainCostsDto("1000", "2000", "3000")); } } \ No newline at end of file