diff --git a/application/src/main/java/de/cotto/lndmanagej/service/TransactionBackgroundLoader.java b/application/src/main/java/de/cotto/lndmanagej/service/TransactionBackgroundLoader.java index a8599c12..da2d0cbc 100644 --- a/application/src/main/java/de/cotto/lndmanagej/service/TransactionBackgroundLoader.java +++ b/application/src/main/java/de/cotto/lndmanagej/service/TransactionBackgroundLoader.java @@ -3,6 +3,7 @@ package de.cotto.lndmanagej.service; import de.cotto.lndmanagej.model.Channel; import de.cotto.lndmanagej.model.ChannelPoint; import de.cotto.lndmanagej.model.ClosedOrClosingChannel; +import de.cotto.lndmanagej.model.ForceClosingChannel; import de.cotto.lndmanagej.transactions.service.TransactionService; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -33,7 +34,9 @@ public class TransactionBackgroundLoader { private Stream getTransactionHashes() { Stream openTransactionHashes = getOpenTransactionHashes(); Stream closeTransactionHashes = getCloseTransactionHashes(); - return Stream.concat(openTransactionHashes, closeTransactionHashes); + Stream htlcOutpointHashes = getHtlcOutpointHashes(); + return Stream.of(openTransactionHashes, closeTransactionHashes, htlcOutpointHashes) + .flatMap(s -> s); } private Stream getOpenTransactionHashes() { @@ -54,4 +57,11 @@ public class TransactionBackgroundLoader { .map(ClosedOrClosingChannel::getCloseTransactionHash); } + private Stream getHtlcOutpointHashes() { + return channelService.getForceClosingChannels().stream() + .map(ForceClosingChannel::getHtlcOutpoints) + .flatMap(Collection::stream) + .map(ChannelPoint::getTransactionHash); + } + } diff --git a/application/src/test/java/de/cotto/lndmanagej/service/TransactionBackgroundLoaderTest.java b/application/src/test/java/de/cotto/lndmanagej/service/TransactionBackgroundLoaderTest.java index 001490ac..a01f0a9f 100644 --- a/application/src/test/java/de/cotto/lndmanagej/service/TransactionBackgroundLoaderTest.java +++ b/application/src/test/java/de/cotto/lndmanagej/service/TransactionBackgroundLoaderTest.java @@ -1,5 +1,6 @@ package de.cotto.lndmanagej.service; +import de.cotto.lndmanagej.model.ChannelPoint; import de.cotto.lndmanagej.model.LocalOpenChannel; import de.cotto.lndmanagej.transactions.service.TransactionService; import org.junit.jupiter.api.Test; @@ -95,8 +96,9 @@ class TransactionBackgroundLoaderTest { @Test void update_from_force_closing_channels() { String transactionHash = FORCE_CLOSING_CHANNEL.getChannelPoint().getTransactionHash(); - when(channelService.getForceClosingChannels()).thenReturn(Set.of(FORCE_CLOSING_CHANNEL)); when(transactionService.isUnknown(transactionHash)).thenReturn(true); + + when(channelService.getForceClosingChannels()).thenReturn(Set.of(FORCE_CLOSING_CHANNEL)); transactionBackgroundLoader.loadTransactionForOneChannel(); verify(transactionService).getTransaction(transactionHash); } @@ -104,14 +106,33 @@ class TransactionBackgroundLoaderTest { @Test void update_from_force_closing_channels_close_transaction() { String closeTransactionHash = FORCE_CLOSING_CHANNEL.getCloseTransactionHash(); - String openTransactionHash = FORCE_CLOSING_CHANNEL.getChannelPoint().getTransactionHash(); - when(channelService.getForceClosingChannels()).thenReturn(Set.of(FORCE_CLOSING_CHANNEL)); - when(transactionService.isUnknown(openTransactionHash)).thenReturn(false); when(transactionService.isUnknown(closeTransactionHash)).thenReturn(true); + + String openTransactionHash = FORCE_CLOSING_CHANNEL.getChannelPoint().getTransactionHash(); + when(transactionService.isUnknown(openTransactionHash)).thenReturn(false); + + when(channelService.getForceClosingChannels()).thenReturn(Set.of(FORCE_CLOSING_CHANNEL)); transactionBackgroundLoader.loadTransactionForOneChannel(); verify(transactionService).getTransaction(closeTransactionHash); } + @Test + void update_from_force_closing_channels_pending_htlc_output() { + String htlcOutpointHash = FORCE_CLOSING_CHANNEL.getHtlcOutpoints().stream() + .map(ChannelPoint::getTransactionHash) + .findFirst() + .orElseThrow(); + when(transactionService.isUnknown(htlcOutpointHash)).thenReturn(true); + + String openTransactionHash = FORCE_CLOSING_CHANNEL.getChannelPoint().getTransactionHash(); + when(transactionService.isUnknown(openTransactionHash)).thenReturn(false); + when(transactionService.isUnknown(FORCE_CLOSING_CHANNEL.getCloseTransactionHash())).thenReturn(false); + + when(channelService.getForceClosingChannels()).thenReturn(Set.of(FORCE_CLOSING_CHANNEL)); + transactionBackgroundLoader.loadTransactionForOneChannel(); + verify(transactionService).getTransaction(htlcOutpointHash); + } + @Test void update_one_unknown() { LocalOpenChannel channel1 = diff --git a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannels.java b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannels.java index f14d212c..4ac633d7 100644 --- a/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannels.java +++ b/grpc-adapter/src/main/java/de/cotto/lndmanagej/grpc/GrpcChannels.java @@ -4,8 +4,8 @@ import de.cotto.lndmanagej.model.BalanceInformation; import de.cotto.lndmanagej.model.ChannelId; import de.cotto.lndmanagej.model.ChannelIdResolver; import de.cotto.lndmanagej.model.ChannelPoint; -import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.ClosedChannel; +import de.cotto.lndmanagej.model.Coins; import de.cotto.lndmanagej.model.ForceClosingChannel; import de.cotto.lndmanagej.model.LocalOpenChannel; import de.cotto.lndmanagej.model.Pubkey; @@ -15,6 +15,7 @@ import lnrpc.ChannelCloseSummary.ClosureType; import lnrpc.PendingChannelsResponse; import lnrpc.PendingChannelsResponse.ForceClosedChannel; import lnrpc.PendingChannelsResponse.PendingChannel; +import lnrpc.PendingHTLC; import org.springframework.stereotype.Component; import java.util.Optional; @@ -99,10 +100,18 @@ public class GrpcChannels { Coins.ofSatoshis(pendingChannel.getCapacity()), ownPubkey, Pubkey.create(pendingChannel.getRemoteNodePub()), - forceClosedChannel.getClosingTxid() + forceClosedChannel.getClosingTxid(), + getHtlcOutpoints(forceClosedChannel) )); } + private Set getHtlcOutpoints(ForceClosedChannel forceClosedChannel) { + return forceClosedChannel.getPendingHtlcsList().stream() + .map(PendingHTLC::getOutpoint) + .map(ChannelPoint::create) + .collect(toSet()); + } + public Optional getChannel(ChannelId channelId) { Pubkey ownPubkey = grpcGetInfo.getPubkey(); long expectedChannelId = channelId.getShortChannelId(); diff --git a/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcChannelsTest.java b/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcChannelsTest.java index d5aaa085..6e30d691 100644 --- a/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcChannelsTest.java +++ b/grpc-adapter/src/test/java/de/cotto/lndmanagej/grpc/GrpcChannelsTest.java @@ -8,6 +8,7 @@ import lnrpc.ChannelCloseSummary; import lnrpc.ChannelConstraints; import lnrpc.PendingChannelsResponse; import lnrpc.PendingChannelsResponse.ForceClosedChannel; +import lnrpc.PendingHTLC; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -33,6 +34,7 @@ import static de.cotto.lndmanagej.model.ClosedChannelFixtures.CLOSED_CHANNEL_2; import static de.cotto.lndmanagej.model.ClosedChannelFixtures.CLOSED_CHANNEL_3; 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.HTLC_OUTPOINT; import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL; import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_2; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; @@ -212,6 +214,7 @@ class GrpcChannelsTest { return ForceClosedChannel.newBuilder() .setChannel(pendingChannel(channelPoint)) .setClosingTxid(TRANSACTION_HASH_3) + .addPendingHtlcs(PendingHTLC.newBuilder().setOutpoint(HTLC_OUTPOINT.toString()).build()) .build(); } diff --git a/model/src/main/java/de/cotto/lndmanagej/model/ForceClosingChannel.java b/model/src/main/java/de/cotto/lndmanagej/model/ForceClosingChannel.java index 675a91b0..f33953e2 100644 --- a/model/src/main/java/de/cotto/lndmanagej/model/ForceClosingChannel.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/ForceClosingChannel.java @@ -1,14 +1,46 @@ package de.cotto.lndmanagej.model; +import java.util.Objects; +import java.util.Set; + public final class ForceClosingChannel extends ClosedOrClosingChannel { + private final Set htlcOutpoints; + public ForceClosingChannel( ChannelId channelId, ChannelPoint channelPoint, Coins capacity, Pubkey ownPubkey, Pubkey remotePubkey, - String closeTransactionHash + String closeTransactionHash, + Set htlcOutpoints ) { super(channelId, channelPoint, capacity, ownPubkey, remotePubkey, closeTransactionHash); + this.htlcOutpoints = htlcOutpoints; + } + + public Set getHtlcOutpoints() { + return htlcOutpoints; + } + + @Override + @SuppressWarnings("CPD-START") + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + if (!super.equals(other)) { + return false; + } + ForceClosingChannel that = (ForceClosingChannel) other; + return Objects.equals(htlcOutpoints, that.htlcOutpoints); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), htlcOutpoints); } } diff --git a/model/src/test/java/de/cotto/lndmanagej/model/ForceClosingChannelTest.java b/model/src/test/java/de/cotto/lndmanagej/model/ForceClosingChannelTest.java index a29911d2..bf7251cd 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/ForceClosingChannelTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/ForceClosingChannelTest.java @@ -8,6 +8,7 @@ import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID; import static de.cotto.lndmanagej.model.ChannelPointFixtures.CHANNEL_POINT; import static de.cotto.lndmanagej.model.ChannelPointFixtures.TRANSACTION_HASH_3; import static de.cotto.lndmanagej.model.ForceClosingChannelFixtures.FORCE_CLOSING_CHANNEL; +import static de.cotto.lndmanagej.model.ForceClosingChannelFixtures.HTLC_OUTPOINTS; 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; @@ -15,8 +16,9 @@ import static org.assertj.core.api.Assertions.assertThat; class ForceClosingChannelTest { @Test void create() { - assertThat(new ForceClosingChannel(CHANNEL_ID, CHANNEL_POINT, CAPACITY, PUBKEY, PUBKEY_2, TRANSACTION_HASH_3)) - .isEqualTo(FORCE_CLOSING_CHANNEL); + assertThat(new ForceClosingChannel( + CHANNEL_ID, CHANNEL_POINT, CAPACITY, PUBKEY, PUBKEY_2, TRANSACTION_HASH_3, HTLC_OUTPOINTS + )).isEqualTo(FORCE_CLOSING_CHANNEL); } @Test @@ -44,6 +46,11 @@ class ForceClosingChannelTest { assertThat(FORCE_CLOSING_CHANNEL.getPubkeys()).containsExactlyInAnyOrder(PUBKEY, PUBKEY_2); } + @Test + void getHtlcOutpoints() { + assertThat(FORCE_CLOSING_CHANNEL.getHtlcOutpoints()).isEqualTo(HTLC_OUTPOINTS); + } + @Test void testEquals() { EqualsVerifier.forClass(ForceClosingChannel.class).usingGetClass().verify(); diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForceClosingChannelFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForceClosingChannelFixtures.java index 3ed3235b..b6eab12c 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForceClosingChannelFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/ForceClosingChannelFixtures.java @@ -1,23 +1,32 @@ package de.cotto.lndmanagej.model; +import java.util.Set; + import static de.cotto.lndmanagej.model.ChannelFixtures.CAPACITY; 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_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_3; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2; import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3; -public class ForceClosingChannelFixtures { - public static final ForceClosingChannel FORCE_CLOSING_CHANNEL = - new ForceClosingChannel(CHANNEL_ID, CHANNEL_POINT, CAPACITY, PUBKEY, PUBKEY_2, TRANSACTION_HASH_3); - public static final ForceClosingChannel FORCE_CLOSING_CHANNEL_2 - = new ForceClosingChannel(CHANNEL_ID_2, CHANNEL_POINT_2, CAPACITY, PUBKEY, PUBKEY_2, TRANSACTION_HASH_3); - public static final ForceClosingChannel FORCE_CLOSING_CHANNEL_3 = - new ForceClosingChannel(CHANNEL_ID_3, CHANNEL_POINT, CAPACITY, PUBKEY, PUBKEY_2, TRANSACTION_HASH_3); - public static final ForceClosingChannel FORCE_CLOSING_CHANNEL_TO_NODE_3 = - new ForceClosingChannel(CHANNEL_ID_3, CHANNEL_POINT, CAPACITY, PUBKEY, PUBKEY_3, TRANSACTION_HASH_3); +public class ForceClosingChannelFixtures { + public static final ChannelPoint HTLC_OUTPOINT = CHANNEL_POINT_3; + public static final Set HTLC_OUTPOINTS = Set.of(HTLC_OUTPOINT); + public static final ForceClosingChannel FORCE_CLOSING_CHANNEL = new ForceClosingChannel( + CHANNEL_ID, CHANNEL_POINT, CAPACITY, PUBKEY, PUBKEY_2, TRANSACTION_HASH_3, HTLC_OUTPOINTS + ); + public static final ForceClosingChannel FORCE_CLOSING_CHANNEL_2 = new ForceClosingChannel( + CHANNEL_ID_2, CHANNEL_POINT_2, CAPACITY, PUBKEY, PUBKEY_2, TRANSACTION_HASH_3, HTLC_OUTPOINTS + ); + public static final ForceClosingChannel FORCE_CLOSING_CHANNEL_3 = new ForceClosingChannel( + CHANNEL_ID_3, CHANNEL_POINT, CAPACITY, PUBKEY, PUBKEY_2, TRANSACTION_HASH_3, HTLC_OUTPOINTS + ); + public static final ForceClosingChannel FORCE_CLOSING_CHANNEL_TO_NODE_3 = new ForceClosingChannel( + CHANNEL_ID_3, CHANNEL_POINT, CAPACITY, PUBKEY, PUBKEY_3, TRANSACTION_HASH_3, HTLC_OUTPOINTS + ); }