mirror of
https://github.com/aljazceru/lnd-manageJ.git
synced 2026-01-20 06:24:28 +01:00
allow users to specify resolutions manually
This commit is contained in:
@@ -37,4 +37,5 @@ feign.client.config.default.readTimeout=10000
|
||||
lndmanagej.macaroon-file=${user.home}/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
lndmanagej.cert-file=${user.home}/.lnd/tls.cert
|
||||
lndmanagej.host=localhost
|
||||
lndmanagej.port=10009
|
||||
lndmanagej.port=10009
|
||||
lndmanagej.hardcoded-path=${user.home}/.config/lnd-manageJ.conf
|
||||
@@ -4,6 +4,7 @@ import com.codahale.metrics.annotation.Timed;
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.ChannelIdResolver;
|
||||
import de.cotto.lndmanagej.model.ChannelPoint;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.model.Transaction;
|
||||
import de.cotto.lndmanagej.transactions.service.TransactionService;
|
||||
import org.slf4j.Logger;
|
||||
@@ -24,7 +25,7 @@ public class ChannelIdResolverImpl implements ChannelIdResolver {
|
||||
@Timed
|
||||
@Override
|
||||
public Optional<ChannelId> resolveFromChannelPoint(ChannelPoint channelPoint) {
|
||||
String transactionHash = channelPoint.getTransactionHash();
|
||||
TransactionHash transactionHash = channelPoint.getTransactionHash();
|
||||
Optional<Transaction> transactionOptional = transactionService.getTransaction(transactionHash);
|
||||
if (transactionOptional.isEmpty()) {
|
||||
logger.warn("Unable resolve transaction ID for {}", transactionHash);
|
||||
|
||||
@@ -11,6 +11,7 @@ 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.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.model.Transaction;
|
||||
import de.cotto.lndmanagej.transactions.service.TransactionService;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -58,7 +59,7 @@ public class OnChainCostService {
|
||||
@Timed
|
||||
public Optional<Coins> getOpenCostsForChannel(LocalChannel localChannel) {
|
||||
if (localChannel.getOpenInitiator().equals(OpenInitiator.LOCAL)) {
|
||||
String openTransactionHash = localChannel.getChannelPoint().getTransactionHash();
|
||||
TransactionHash openTransactionHash = localChannel.getChannelPoint().getTransactionHash();
|
||||
return transactionService.getTransaction(openTransactionHash)
|
||||
.map(Transaction::fees)
|
||||
.map(Coins::satoshis)
|
||||
@@ -114,11 +115,11 @@ public class OnChainCostService {
|
||||
.reduce(Coins.NONE, Coins::add);
|
||||
}
|
||||
|
||||
private long getNumberOfChannelsWithOpenTransactionHash(String openTransactionHash) {
|
||||
private long getNumberOfChannelsWithOpenTransactionHash(TransactionHash openTransactionHash) {
|
||||
return channelService.getAllLocalChannels()
|
||||
.map(LocalChannel::getChannelPoint)
|
||||
.map(ChannelPoint::getTransactionHash)
|
||||
.filter(x -> x.equals(openTransactionHash))
|
||||
.filter(hash -> hash.equals(openTransactionHash))
|
||||
.count();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.cotto.lndmanagej.service;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import de.cotto.lndmanagej.model.OpenInitiator;
|
||||
import de.cotto.lndmanagej.model.OpenInitiatorResolver;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.service.TransactionService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -16,7 +17,7 @@ public class OpenInitiatorResolverImpl implements OpenInitiatorResolver {
|
||||
|
||||
@Timed
|
||||
@Override
|
||||
public OpenInitiator resolveFromOpenTransactionHash(String transactionHash) {
|
||||
public OpenInitiator resolveFromOpenTransactionHash(TransactionHash transactionHash) {
|
||||
Boolean knownByLnd = transactionService.isKnownByLnd(transactionHash).orElse(null);
|
||||
if (knownByLnd == null) {
|
||||
return OpenInitiator.UNKNOWN;
|
||||
|
||||
@@ -6,6 +6,7 @@ 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.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.service.TransactionService;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -34,15 +35,15 @@ public class TransactionBackgroundLoader {
|
||||
.ifPresent(transactionService::getTransaction);
|
||||
}
|
||||
|
||||
private Stream<String> getTransactionHashes() {
|
||||
Stream<String> openTransactionHashes = getOpenTransactionHashes();
|
||||
Stream<String> closeTransactionHashes = getCloseTransactionHashes();
|
||||
Stream<String> sweepTransactionHashes = getSweepTransactionHashes();
|
||||
private Stream<TransactionHash> getTransactionHashes() {
|
||||
Stream<TransactionHash> openTransactionHashes = getOpenTransactionHashes();
|
||||
Stream<TransactionHash> closeTransactionHashes = getCloseTransactionHashes();
|
||||
Stream<TransactionHash> sweepTransactionHashes = getSweepTransactionHashes();
|
||||
return Stream.of(openTransactionHashes, closeTransactionHashes, sweepTransactionHashes)
|
||||
.flatMap(s -> s);
|
||||
}
|
||||
|
||||
private Stream<String> getOpenTransactionHashes() {
|
||||
private Stream<TransactionHash> getOpenTransactionHashes() {
|
||||
return Stream.of(
|
||||
channelService.getOpenChannels(),
|
||||
channelService.getClosedChannels(),
|
||||
@@ -54,13 +55,13 @@ public class TransactionBackgroundLoader {
|
||||
.map(ChannelPoint::getTransactionHash);
|
||||
}
|
||||
|
||||
private Stream<String> getCloseTransactionHashes() {
|
||||
private Stream<TransactionHash> getCloseTransactionHashes() {
|
||||
return Stream.of(channelService.getClosedChannels(), channelService.getForceClosingChannels())
|
||||
.flatMap(Collection::stream)
|
||||
.map(ClosedOrClosingChannel::getCloseTransactionHash);
|
||||
}
|
||||
|
||||
private Stream<String> getSweepTransactionHashes() {
|
||||
private Stream<TransactionHash> getSweepTransactionHashes() {
|
||||
return channelService.getClosedChannels().stream()
|
||||
.filter(ClosedChannel::isForceClosed)
|
||||
.map(ClosedChannel::getAsForceClosedChannel)
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.cotto.lndmanagej.service;
|
||||
import de.cotto.lndmanagej.model.ChannelCoreInformation;
|
||||
import de.cotto.lndmanagej.model.ChannelPoint;
|
||||
import de.cotto.lndmanagej.model.LocalOpenChannel;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.service.TransactionService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -76,7 +77,7 @@ class TransactionBackgroundLoaderTest {
|
||||
|
||||
@Test
|
||||
void update_from_closed_channels() {
|
||||
String transactionHash = CLOSED_CHANNEL.getChannelPoint().getTransactionHash();
|
||||
TransactionHash transactionHash = CLOSED_CHANNEL.getChannelPoint().getTransactionHash();
|
||||
when(channelService.getClosedChannels()).thenReturn(Set.of(CLOSED_CHANNEL));
|
||||
when(transactionService.isUnknown(transactionHash)).thenReturn(true);
|
||||
transactionBackgroundLoader.loadTransactionForOneChannel();
|
||||
@@ -85,7 +86,7 @@ class TransactionBackgroundLoaderTest {
|
||||
|
||||
@Test
|
||||
void update_from_closed_channels_close_transaction() {
|
||||
String closeTransactionHash = CLOSED_CHANNEL.getCloseTransactionHash();
|
||||
TransactionHash closeTransactionHash = CLOSED_CHANNEL.getCloseTransactionHash();
|
||||
when(channelService.getClosedChannels()).thenReturn(Set.of(CLOSED_CHANNEL));
|
||||
when(transactionService.isUnknown(CLOSED_CHANNEL.getChannelPoint().getTransactionHash())).thenReturn(false);
|
||||
when(transactionService.isUnknown(closeTransactionHash)).thenReturn(true);
|
||||
@@ -95,7 +96,7 @@ class TransactionBackgroundLoaderTest {
|
||||
|
||||
@Test
|
||||
void update_from_waiting_close_channels() {
|
||||
String transactionHash = WAITING_CLOSE_CHANNEL.getChannelPoint().getTransactionHash();
|
||||
TransactionHash transactionHash = WAITING_CLOSE_CHANNEL.getChannelPoint().getTransactionHash();
|
||||
when(channelService.getWaitingCloseChannels()).thenReturn(Set.of(WAITING_CLOSE_CHANNEL));
|
||||
when(transactionService.isUnknown(transactionHash)).thenReturn(true);
|
||||
transactionBackgroundLoader.loadTransactionForOneChannel();
|
||||
@@ -104,7 +105,7 @@ class TransactionBackgroundLoaderTest {
|
||||
|
||||
@Test
|
||||
void update_from_force_closing_channels() {
|
||||
String transactionHash = FORCE_CLOSING_CHANNEL.getChannelPoint().getTransactionHash();
|
||||
TransactionHash transactionHash = FORCE_CLOSING_CHANNEL.getChannelPoint().getTransactionHash();
|
||||
when(transactionService.isUnknown(transactionHash)).thenReturn(true);
|
||||
|
||||
when(channelService.getForceClosingChannels()).thenReturn(Set.of(FORCE_CLOSING_CHANNEL));
|
||||
@@ -114,10 +115,10 @@ class TransactionBackgroundLoaderTest {
|
||||
|
||||
@Test
|
||||
void update_from_force_closing_channels_close_transaction() {
|
||||
String closeTransactionHash = FORCE_CLOSING_CHANNEL.getCloseTransactionHash();
|
||||
TransactionHash closeTransactionHash = FORCE_CLOSING_CHANNEL.getCloseTransactionHash();
|
||||
when(transactionService.isUnknown(closeTransactionHash)).thenReturn(true);
|
||||
|
||||
String openTransactionHash = FORCE_CLOSING_CHANNEL.getChannelPoint().getTransactionHash();
|
||||
TransactionHash openTransactionHash = FORCE_CLOSING_CHANNEL.getChannelPoint().getTransactionHash();
|
||||
when(transactionService.isUnknown(openTransactionHash)).thenReturn(false);
|
||||
|
||||
when(channelService.getForceClosingChannels()).thenReturn(Set.of(FORCE_CLOSING_CHANNEL));
|
||||
@@ -127,12 +128,12 @@ class TransactionBackgroundLoaderTest {
|
||||
|
||||
@Test
|
||||
void update_from_force_closing_channels_ignores_pending_htlc_output() {
|
||||
String htlcOutpointHash = FORCE_CLOSING_CHANNEL.getHtlcOutpoints().stream()
|
||||
TransactionHash htlcOutpointHash = FORCE_CLOSING_CHANNEL.getHtlcOutpoints().stream()
|
||||
.map(ChannelPoint::getTransactionHash)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
String openTransactionHash = FORCE_CLOSING_CHANNEL.getChannelPoint().getTransactionHash();
|
||||
TransactionHash openTransactionHash = FORCE_CLOSING_CHANNEL.getChannelPoint().getTransactionHash();
|
||||
when(transactionService.isUnknown(openTransactionHash)).thenReturn(false);
|
||||
when(transactionService.isUnknown(FORCE_CLOSING_CHANNEL.getCloseTransactionHash())).thenReturn(false);
|
||||
|
||||
@@ -196,7 +197,7 @@ class TransactionBackgroundLoaderTest {
|
||||
true
|
||||
);
|
||||
when(channelService.getOpenChannels()).thenReturn(Set.of(channel1, channel2, channel3));
|
||||
String unknownHash = CHANNEL_POINT_3.getTransactionHash();
|
||||
TransactionHash unknownHash = CHANNEL_POINT_3.getTransactionHash();
|
||||
when(transactionService.isUnknown(any())).thenReturn(false);
|
||||
when(transactionService.isUnknown(unknownHash)).thenReturn(true);
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':grpc-client')
|
||||
implementation project(':model')
|
||||
implementation project(':caching')
|
||||
implementation project(':grpc-client')
|
||||
implementation project(':hardcoded')
|
||||
implementation project(':model')
|
||||
testImplementation testFixtures(project(':model'))
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import de.cotto.lndmanagej.model.ForceClosingChannel;
|
||||
import de.cotto.lndmanagej.model.LocalOpenChannel;
|
||||
import de.cotto.lndmanagej.model.OpenInitiator;
|
||||
import de.cotto.lndmanagej.model.Pubkey;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.model.WaitingCloseChannel;
|
||||
import lnrpc.Channel;
|
||||
import lnrpc.PendingChannelsResponse;
|
||||
@@ -92,7 +93,7 @@ public class GrpcChannels extends GrpcChannelsBase {
|
||||
new ChannelCoreInformation(id, channelPoint, Coins.ofSatoshis(pendingChannel.getCapacity())),
|
||||
ownPubkey,
|
||||
Pubkey.create(pendingChannel.getRemoteNodePub()),
|
||||
forceClosedChannel.getClosingTxid(),
|
||||
TransactionHash.create(forceClosedChannel.getClosingTxid()),
|
||||
getHtlcOutpoints(forceClosedChannel),
|
||||
getOpenInitiator(pendingChannel.getInitiator())
|
||||
));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package de.cotto.lndmanagej.grpc;
|
||||
|
||||
import de.cotto.lndmanagej.hardcoded.HardcodedService;
|
||||
import de.cotto.lndmanagej.model.BreachForceClosedChannelBuilder;
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.ChannelIdResolver;
|
||||
import de.cotto.lndmanagej.model.ChannelPoint;
|
||||
import de.cotto.lndmanagej.model.CloseInitiator;
|
||||
@@ -13,6 +15,7 @@ import de.cotto.lndmanagej.model.OpenInitiator;
|
||||
import de.cotto.lndmanagej.model.OpenInitiatorResolver;
|
||||
import de.cotto.lndmanagej.model.Pubkey;
|
||||
import de.cotto.lndmanagej.model.Resolution;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import lnrpc.ChannelCloseSummary;
|
||||
import lnrpc.ChannelCloseSummary.ClosureType;
|
||||
import lnrpc.Initiator;
|
||||
@@ -20,6 +23,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
import static lnrpc.ChannelCloseSummary.ClosureType.LOCAL_FORCE_CLOSE;
|
||||
@@ -33,17 +37,20 @@ public class GrpcClosedChannels extends GrpcChannelsBase {
|
||||
private final GrpcService grpcService;
|
||||
private final GrpcGetInfo grpcGetInfo;
|
||||
private final OpenInitiatorResolver openInitiatorResolver;
|
||||
private final HardcodedService hardcodedService;
|
||||
|
||||
public GrpcClosedChannels(
|
||||
GrpcService grpcService,
|
||||
GrpcGetInfo grpcGetInfo,
|
||||
ChannelIdResolver channelIdResolver,
|
||||
OpenInitiatorResolver openInitiatorResolver
|
||||
OpenInitiatorResolver openInitiatorResolver,
|
||||
HardcodedService hardcodedService
|
||||
) {
|
||||
super(channelIdResolver);
|
||||
this.grpcService = grpcService;
|
||||
this.grpcGetInfo = grpcGetInfo;
|
||||
this.openInitiatorResolver = openInitiatorResolver;
|
||||
this.hardcodedService = hardcodedService;
|
||||
}
|
||||
|
||||
public Set<ClosedChannel> getClosedChannels() {
|
||||
@@ -86,33 +93,34 @@ public class GrpcClosedChannels extends GrpcChannelsBase {
|
||||
.withCapacity(Coins.ofSatoshis(channelCloseSummary.getCapacity()))
|
||||
.withOwnPubkey(ownPubkey)
|
||||
.withRemotePubkey(Pubkey.create(channelCloseSummary.getRemotePubkey()))
|
||||
.withCloseTransactionHash(channelCloseSummary.getClosingTxHash())
|
||||
.withCloseTransactionHash(TransactionHash.create(channelCloseSummary.getClosingTxHash()))
|
||||
.withOpenInitiator(openInitiator)
|
||||
.withCloseHeight(channelCloseSummary.getCloseHeight())
|
||||
.withResolutions(getResolutions(channelCloseSummary))
|
||||
.withResolutions(getResolutions(channelId, channelCloseSummary))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private Set<Resolution> getResolutions(ChannelCloseSummary channelCloseSummary) {
|
||||
return channelCloseSummary.getResolutionsList().stream()
|
||||
private Set<Resolution> getResolutions(ChannelId channelId, ChannelCloseSummary channelCloseSummary) {
|
||||
Stream<Resolution> hardcodedResolutions = hardcodedService.getResolutions(channelId).stream();
|
||||
Stream<Resolution> resolutions = channelCloseSummary.getResolutionsList().stream()
|
||||
.map(lndResolution -> {
|
||||
Optional<String> sweepTransaction;
|
||||
Optional<TransactionHash> sweepTransaction;
|
||||
if (lndResolution.getSweepTxid().isBlank()) {
|
||||
sweepTransaction = Optional.empty();
|
||||
} else {
|
||||
sweepTransaction = Optional.of(lndResolution.getSweepTxid());
|
||||
sweepTransaction = Optional.of(TransactionHash.create(lndResolution.getSweepTxid()));
|
||||
}
|
||||
return new Resolution(
|
||||
sweepTransaction,
|
||||
lndResolution.getResolutionType().name(),
|
||||
lndResolution.getOutcome().name()
|
||||
);
|
||||
})
|
||||
.collect(toSet());
|
||||
});
|
||||
return Stream.concat(hardcodedResolutions, resolutions).collect(toSet());
|
||||
}
|
||||
|
||||
private OpenInitiator getOpenInitiator(Initiator initiator, String transactionHash) {
|
||||
private OpenInitiator getOpenInitiator(Initiator initiator, TransactionHash transactionHash) {
|
||||
OpenInitiator openInitiator = getOpenInitiator(initiator);
|
||||
if (openInitiator.equals(OpenInitiator.UNKNOWN)) {
|
||||
return openInitiatorResolver.resolveFromOpenTransactionHash(transactionHash);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package de.cotto.lndmanagej.grpc;
|
||||
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import lnrpc.Transaction;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
|
||||
@Component
|
||||
public class GrpcTransactions {
|
||||
@@ -16,14 +18,15 @@ public class GrpcTransactions {
|
||||
this.grpcService = grpcService;
|
||||
}
|
||||
|
||||
public Optional<Set<String>> getKnownTransactionHashesInBlock(int blockHeight) {
|
||||
public Optional<Set<TransactionHash>> getKnownTransactionHashesInBlock(int blockHeight) {
|
||||
List<Transaction> transactionsInBlock = getTransactionsInBlock(blockHeight).orElse(null);
|
||||
if (transactionsInBlock == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Set<String> hashes = transactionsInBlock.stream()
|
||||
Set<TransactionHash> hashes = transactionsInBlock.stream()
|
||||
.map(Transaction::getTxHash)
|
||||
.collect(Collectors.toSet());
|
||||
.map(TransactionHash::create)
|
||||
.collect(toSet());
|
||||
return Optional.of(hashes);
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ class GrpcChannelsTest {
|
||||
private ForceClosedChannel forceClosingChannel(ChannelPoint channelPoint, Initiator initiator) {
|
||||
return ForceClosedChannel.newBuilder()
|
||||
.setChannel(pendingChannel(channelPoint, initiator))
|
||||
.setClosingTxid(TRANSACTION_HASH_3)
|
||||
.setClosingTxid(TRANSACTION_HASH_3.getHash())
|
||||
.addPendingHtlcs(PendingHTLC.newBuilder().setOutpoint(HTLC_OUTPOINT.toString()).build())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.cotto.lndmanagej.grpc;
|
||||
|
||||
import de.cotto.lndmanagej.hardcoded.HardcodedService;
|
||||
import de.cotto.lndmanagej.model.ChannelIdResolver;
|
||||
import de.cotto.lndmanagej.model.CloseInitiator;
|
||||
import de.cotto.lndmanagej.model.ClosedChannelFixtures;
|
||||
@@ -7,6 +8,7 @@ import de.cotto.lndmanagej.model.ForceClosedChannelBuilder;
|
||||
import de.cotto.lndmanagej.model.OpenInitiator;
|
||||
import de.cotto.lndmanagej.model.OpenInitiatorResolver;
|
||||
import de.cotto.lndmanagej.model.Resolution;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import lnrpc.ChannelCloseSummary;
|
||||
import lnrpc.Initiator;
|
||||
import lnrpc.ResolutionOutcome;
|
||||
@@ -23,6 +25,7 @@ import java.util.Optional;
|
||||
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_2_SHORT;
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_SHORT;
|
||||
@@ -73,6 +76,9 @@ class GrpcClosedChannelsTest {
|
||||
@Mock
|
||||
private OpenInitiatorResolver openInitiatorResolver;
|
||||
|
||||
@Mock
|
||||
private HardcodedService hardcodedService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY);
|
||||
@@ -166,6 +172,17 @@ class GrpcClosedChannelsTest {
|
||||
.containsExactlyInAnyOrder(FORCE_CLOSED_CHANNEL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getClosedChannels_force_close_with_hardcoded_resolutions() {
|
||||
when(hardcodedService.getResolutions(CHANNEL_ID)).thenReturn(Set.of(COMMIT_CLAIMED));
|
||||
Set<Resolution> resolutions = Set.of(INCOMING_HTLC_CLAIMED);
|
||||
when(grpcService.getClosedChannels()).thenReturn(List.of(
|
||||
closedChannel(CHANNEL_ID_SHORT, REMOTE_FORCE_CLOSE, INITIATOR_LOCAL, INITIATOR_REMOTE, resolutions)
|
||||
));
|
||||
assertThat(grpcClosedChannels.getClosedChannels())
|
||||
.containsExactlyInAnyOrder(FORCE_CLOSED_CHANNEL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getClosedChannels_with_zero_channel_id_not_resolved() {
|
||||
when(grpcService.getClosedChannels()).thenReturn(List.of(
|
||||
@@ -226,7 +243,7 @@ class GrpcClosedChannelsTest {
|
||||
.setRemotePubkey(PUBKEY_2.toString())
|
||||
.setCapacity(CAPACITY.satoshis())
|
||||
.setChannelPoint(CHANNEL_POINT.toString())
|
||||
.setClosingTxHash(TRANSACTION_HASH_2)
|
||||
.setClosingTxHash(TRANSACTION_HASH_2.getHash())
|
||||
.setCloseType(closeType)
|
||||
.setOpenInitiator(openInitiator)
|
||||
.setCloseInitiator(closeInitiator)
|
||||
@@ -238,7 +255,7 @@ class GrpcClosedChannelsTest {
|
||||
private void addResolutions(ChannelCloseSummary.Builder builder, Set<Resolution> resolutions) {
|
||||
resolutions.stream().map(
|
||||
resolution -> lnrpc.Resolution.newBuilder()
|
||||
.setSweepTxid(resolution.sweepTransaction().orElse(""))
|
||||
.setSweepTxid(resolution.sweepTransaction().map(TransactionHash::getHash).orElse(""))
|
||||
.setOutcome(ResolutionOutcome.valueOf(resolution.outcome()))
|
||||
.setResolutionType(ResolutionType.valueOf(resolution.resolutionType()))
|
||||
.build()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.cotto.lndmanagej.grpc;
|
||||
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import lnrpc.Transaction;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -17,9 +18,12 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GrpcTransactionsTest {
|
||||
private static final int BLOCK_HEIGHT = 123_456;
|
||||
private static final String HASH = "abc";
|
||||
private static final String HASH_2 = "def";
|
||||
private static final String HASH_3 = "ghi";
|
||||
private static final TransactionHash HASH =
|
||||
TransactionHash.create("abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabca");
|
||||
private static final TransactionHash HASH_2 =
|
||||
TransactionHash.create("00000000000cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabca");
|
||||
private static final TransactionHash HASH_3 =
|
||||
TransactionHash.create("11111111111cabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabca");
|
||||
private static final Transaction LND_TRANSACTION = transaction(HASH, BLOCK_HEIGHT);
|
||||
private static final Transaction LND_TRANSACTION_2 = transaction(HASH_2, BLOCK_HEIGHT);
|
||||
private static final Transaction LND_TRANSACTION_WRONG_BLOCK = transaction(HASH_3, BLOCK_HEIGHT + 1);
|
||||
@@ -49,7 +53,7 @@ class GrpcTransactionsTest {
|
||||
assertThat(grpcTransactions.getKnownTransactionHashesInBlock(BLOCK_HEIGHT)).contains(Set.of(HASH, HASH_2));
|
||||
}
|
||||
|
||||
private static Transaction transaction(String hash, int blockHeight) {
|
||||
return Transaction.newBuilder().setTxHash(hash).setBlockHeight(blockHeight).build();
|
||||
private static Transaction transaction(TransactionHash hash, int blockHeight) {
|
||||
return Transaction.newBuilder().setTxHash(hash.getHash()).setBlockHeight(blockHeight).build();
|
||||
}
|
||||
}
|
||||
10
hardcoded/build.gradle
Normal file
10
hardcoded/build.gradle
Normal file
@@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
id 'lnd-manageJ.java-library-conventions'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':model')
|
||||
implementation project(':caching')
|
||||
implementation 'org.ini4j:ini4j:0.5.4'
|
||||
testImplementation testFixtures(project(':model'))
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.cotto.lndmanagej.hardcoded;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import de.cotto.lndmanagej.model.ChannelId;
|
||||
import de.cotto.lndmanagej.model.Resolution;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
|
||||
@Component
|
||||
public class HardcodedService {
|
||||
private static final int EXPECTED_NUMBER_OF_COMPONENTS = 3;
|
||||
private static final String RESOLUTIONS_SECTION = "resolutions";
|
||||
private static final Splitter SPLITTER = Splitter.on(":");
|
||||
|
||||
private final IniFileReader iniFileReader;
|
||||
|
||||
public HardcodedService(IniFileReader iniFileReader) {
|
||||
this.iniFileReader = iniFileReader;
|
||||
}
|
||||
|
||||
public Set<Resolution> getResolutions(ChannelId channelId) {
|
||||
Map<String, Set<String>> values = iniFileReader.getValues(RESOLUTIONS_SECTION);
|
||||
Set<String> forShortChannelId = values.getOrDefault(String.valueOf(channelId.getShortChannelId()), Set.of());
|
||||
Set<String> forCompactForm = values.getOrDefault(channelId.getCompactForm(), Set.of());
|
||||
Set<String> forCompactFormLnd = values.getOrDefault(channelId.getCompactFormLnd(), Set.of());
|
||||
return Stream.of(forShortChannelId, forCompactForm, forCompactFormLnd)
|
||||
.flatMap(Set::stream)
|
||||
.map(this::parseResolution)
|
||||
.flatMap(Optional::stream)
|
||||
.collect(toSet());
|
||||
}
|
||||
|
||||
private Optional<Resolution> parseResolution(String encodedResolution) {
|
||||
try {
|
||||
List<String> split = SPLITTER.splitToList(encodedResolution);
|
||||
if (split.size() != EXPECTED_NUMBER_OF_COMPONENTS) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String resolutionType = split.get(0);
|
||||
String outcome = split.get(1);
|
||||
TransactionHash sweepTransaction = TransactionHash.create(split.get(2));
|
||||
return Optional.of(new Resolution(Optional.of(sweepTransaction), resolutionType, outcome));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.cotto.lndmanagej.hardcoded;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import de.cotto.lndmanagej.caching.CacheBuilder;
|
||||
import org.ini4j.Ini;
|
||||
import org.ini4j.Profile;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class IniFileReader {
|
||||
private final String path;
|
||||
private final LoadingCache<String, Map<String, Set<String>>> cache;
|
||||
|
||||
public IniFileReader(@Value("${lndmanagej.hardcoded-path:}") String path) {
|
||||
this.path = path;
|
||||
cache = new CacheBuilder()
|
||||
.withRefresh(Duration.ofSeconds(5))
|
||||
.withExpiry(Duration.ofSeconds(10))
|
||||
.build(this::getValuesWithoutCache);
|
||||
}
|
||||
|
||||
public Map<String, Set<String>> getValues(String sectionName) {
|
||||
return cache.get(sectionName);
|
||||
}
|
||||
|
||||
private Map<String, Set<String>> getValuesWithoutCache(String sectionName) {
|
||||
return getIni().map(ini -> ini.get(sectionName))
|
||||
.map(this::toMultiValueMap)
|
||||
.orElse(Map.of());
|
||||
}
|
||||
|
||||
private Map<String, Set<String>> toMultiValueMap(Profile.Section section) {
|
||||
LinkedHashMap<String, Set<String>> result = new LinkedHashMap<>();
|
||||
for (String key : section.keySet()) {
|
||||
result.put(key, new HashSet<>(section.getAll(key)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Optional<Ini> getIni() {
|
||||
try {
|
||||
return Optional.of(new Ini(new File(path)));
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package de.cotto.lndmanagej.hardcoded;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
|
||||
import static de.cotto.lndmanagej.model.ResolutionFixtures.ANCHOR_CLAIMED;
|
||||
import static de.cotto.lndmanagej.model.ResolutionFixtures.COMMIT_CLAIMED;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HardcodedServiceTest {
|
||||
private static final String SECTION = "resolutions";
|
||||
private static final String COMMIT_CLAIMED_STRING
|
||||
= "COMMIT:CLAIMED:abc222abc000abc000abc000abc000abc000abc000abc000abc000abc000abc0";
|
||||
private static final String ANCHOR_CLAIMED_STRING
|
||||
= "ANCHOR:CLAIMED:abc222abc000abc000abc000abc000abc000abc000abc000abc000abc000abc0";
|
||||
|
||||
@InjectMocks
|
||||
private HardcodedService hardcodedService;
|
||||
|
||||
@Mock
|
||||
private IniFileReader iniFileReader;
|
||||
|
||||
@Test
|
||||
void getResolutions_empty() {
|
||||
when(iniFileReader.getValues(SECTION)).thenReturn(Map.of());
|
||||
assertThat(hardcodedService.getResolutions(CHANNEL_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResolutions_one_resolution_short_channel_id() {
|
||||
when(iniFileReader.getValues(SECTION))
|
||||
.thenReturn(Map.of(String.valueOf(CHANNEL_ID.getShortChannelId()), Set.of(COMMIT_CLAIMED_STRING)));
|
||||
assertThat(hardcodedService.getResolutions(CHANNEL_ID)).containsExactly(COMMIT_CLAIMED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResolutions_one_resolution_compact_form() {
|
||||
when(iniFileReader.getValues(SECTION))
|
||||
.thenReturn(Map.of(CHANNEL_ID.getCompactForm(), Set.of(COMMIT_CLAIMED_STRING)));
|
||||
assertThat(hardcodedService.getResolutions(CHANNEL_ID)).containsExactly(COMMIT_CLAIMED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResolutions_one_resolution_compact_form_lnd() {
|
||||
when(iniFileReader.getValues(SECTION))
|
||||
.thenReturn(Map.of(CHANNEL_ID.getCompactFormLnd(), Set.of(COMMIT_CLAIMED_STRING)));
|
||||
assertThat(hardcodedService.getResolutions(CHANNEL_ID)).containsExactly(COMMIT_CLAIMED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResolutions_two_resolutions() {
|
||||
when(iniFileReader.getValues(SECTION)).thenReturn(Map.of(
|
||||
CHANNEL_ID.getCompactFormLnd(),
|
||||
Set.of(COMMIT_CLAIMED_STRING, ANCHOR_CLAIMED_STRING)
|
||||
));
|
||||
assertThat(hardcodedService.getResolutions(CHANNEL_ID))
|
||||
.containsExactlyInAnyOrder(COMMIT_CLAIMED, ANCHOR_CLAIMED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResolutions_bogus_string() {
|
||||
when(iniFileReader.getValues(SECTION)).thenReturn(Map.of(
|
||||
CHANNEL_ID.getCompactFormLnd(),
|
||||
Set.of("hello", "hello:peter", "a:b:c")
|
||||
));
|
||||
assertThat(hardcodedService.getResolutions(CHANNEL_ID)).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package de.cotto.lndmanagej.hardcoded;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IniFileReaderTest {
|
||||
|
||||
private static final String SECTION = "section";
|
||||
private static final String SECTION_2 = "another-section";
|
||||
|
||||
@Test
|
||||
void file_does_not_exist() throws IOException {
|
||||
File file = createTempFile();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
IniFileReader iniFileReader = new IniFileReader(file.getPath());
|
||||
assertThat(iniFileReader.getValues(SECTION)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void path_does_not_exist() {
|
||||
IniFileReader iniFileReader = new IniFileReader("/blabla/this/does/not/exist/foo.conf");
|
||||
assertThat(iniFileReader.getValues(SECTION)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void empty_file() throws IOException {
|
||||
File file = createTempFile();
|
||||
IniFileReader iniFileReader = new IniFileReader(file.getPath());
|
||||
assertThat(iniFileReader.getValues(SECTION)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void section_without_values() throws IOException {
|
||||
File file = createTempFile();
|
||||
addLineToFile(file, "[" + SECTION + "]");
|
||||
IniFileReader iniFileReader = new IniFileReader(file.getPath());
|
||||
assertThat(iniFileReader.getValues(SECTION)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void section_with_value() throws IOException {
|
||||
File file = createTempFile();
|
||||
addLineToFile(file, "[" + SECTION + "]", "x=y");
|
||||
IniFileReader iniFileReader = new IniFileReader(file.getPath());
|
||||
assertThat(iniFileReader.getValues(SECTION)).isEqualTo(Map.of("x", Set.of("y")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void section_with_two_values() throws IOException {
|
||||
File file = createTempFile();
|
||||
addLineToFile(file, "[" + SECTION + "]", "x=y", "a=b");
|
||||
IniFileReader iniFileReader = new IniFileReader(file.getPath());
|
||||
assertThat(iniFileReader.getValues(SECTION)).isEqualTo(Map.of("x", Set.of("y"), "a", Set.of("b")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void two_sections_with_two_values() throws IOException {
|
||||
File file = createTempFile();
|
||||
addLineToFile(file, "[" + SECTION + "]", "x=y", "a=b", "[" + SECTION_2 + "]", "x=1", "a=2");
|
||||
IniFileReader iniFileReader = new IniFileReader(file.getPath());
|
||||
assertThat(iniFileReader.getValues(SECTION)).isEqualTo(Map.of("x", Set.of("y"), "a", Set.of("b")));
|
||||
assertThat(iniFileReader.getValues(SECTION_2)).isEqualTo(Map.of("x", Set.of("1"), "a", Set.of("2")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void section_with_two_values_for_key() throws IOException {
|
||||
File file = createTempFile();
|
||||
addLineToFile(file, "[" + SECTION + "]", "a=y", "a=b");
|
||||
IniFileReader iniFileReader = new IniFileReader(file.getPath());
|
||||
assertThat(iniFileReader.getValues(SECTION)).isEqualTo(Map.of("a", Set.of("y", "b")));
|
||||
}
|
||||
|
||||
private void addLineToFile(File file, String... lines) throws IOException {
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(file.toPath())) {
|
||||
for (String line : lines) {
|
||||
writer.write(line + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File createTempFile() throws IOException {
|
||||
return File.createTempFile("hardcoded", "temp");
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ public class BreachForceClosedChannel extends ForceClosedChannel {
|
||||
ChannelCoreInformation channelCoreInformation,
|
||||
Pubkey ownPubkey,
|
||||
Pubkey remotePubkey,
|
||||
String closeTransactionHash,
|
||||
TransactionHash closeTransactionHash,
|
||||
OpenInitiator openInitiator,
|
||||
int closeHeight,
|
||||
Set<Resolution> resolutions
|
||||
|
||||
@@ -8,20 +8,20 @@ import java.util.Objects;
|
||||
public final class ChannelPoint {
|
||||
private static final Splitter SPLITTER = Splitter.on(":");
|
||||
|
||||
private final String transactionHash;
|
||||
private final TransactionHash transactionHash;
|
||||
private final int output;
|
||||
|
||||
private ChannelPoint(String transactionHash, Integer output) {
|
||||
private ChannelPoint(TransactionHash transactionHash, Integer output) {
|
||||
this.transactionHash = transactionHash;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
public static ChannelPoint create(String channelPoint) {
|
||||
List<String> split = SPLITTER.splitToList(channelPoint);
|
||||
return new ChannelPoint(split.get(0), Integer.valueOf(split.get(1)));
|
||||
return new ChannelPoint(TransactionHash.create(split.get(0)), Integer.valueOf(split.get(1)));
|
||||
}
|
||||
|
||||
public String getTransactionHash() {
|
||||
public TransactionHash getTransactionHash() {
|
||||
return transactionHash;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,6 @@ public final class ChannelPoint {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return transactionHash + ':' + output;
|
||||
return transactionHash.toString() + ':' + output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public abstract class ClosedChannel extends ClosedOrClosingChannel {
|
||||
ChannelCoreInformation channelCoreInformation,
|
||||
Pubkey ownPubkey,
|
||||
Pubkey remotePubkey,
|
||||
String closeTransactionHash,
|
||||
TransactionHash closeTransactionHash,
|
||||
OpenInitiator openInitiator,
|
||||
CloseInitiator closeInitiator,
|
||||
int closeHeight
|
||||
|
||||
@@ -23,7 +23,7 @@ public abstract class ClosedChannelBuilder<T extends ClosedChannel> {
|
||||
Pubkey remotePubkey;
|
||||
|
||||
@Nullable
|
||||
String closeTransactionHash;
|
||||
TransactionHash closeTransactionHash;
|
||||
|
||||
@Nullable
|
||||
OpenInitiator openInitiator;
|
||||
@@ -64,7 +64,7 @@ public abstract class ClosedChannelBuilder<T extends ClosedChannel> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClosedChannelBuilder<T> withCloseTransactionHash(String closeTransactionHash) {
|
||||
public ClosedChannelBuilder<T> withCloseTransactionHash(TransactionHash closeTransactionHash) {
|
||||
this.closeTransactionHash = closeTransactionHash;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@ package de.cotto.lndmanagej.model;
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class ClosedOrClosingChannel extends LocalChannel {
|
||||
private final String closeTransactionHash;
|
||||
private final TransactionHash closeTransactionHash;
|
||||
|
||||
protected ClosedOrClosingChannel(
|
||||
ChannelCoreInformation channelCoreInformation,
|
||||
Pubkey ownPubkey,
|
||||
Pubkey remotePubkey,
|
||||
String closeTransactionHash,
|
||||
TransactionHash closeTransactionHash,
|
||||
OpenInitiator openInitiator
|
||||
) {
|
||||
super(channelCoreInformation, ownPubkey, remotePubkey, openInitiator, false);
|
||||
this.closeTransactionHash = closeTransactionHash;
|
||||
}
|
||||
|
||||
public String getCloseTransactionHash() {
|
||||
public TransactionHash getCloseTransactionHash() {
|
||||
return closeTransactionHash;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ public class CoopClosedChannel extends ClosedChannel {
|
||||
ChannelCoreInformation channelCoreInformation,
|
||||
Pubkey ownPubkey,
|
||||
Pubkey remotePubkey,
|
||||
String closeTransactionHash,
|
||||
TransactionHash closeTransactionHash,
|
||||
OpenInitiator openInitiator,
|
||||
CloseInitiator closeInitiator,
|
||||
int closeHeight
|
||||
|
||||
@@ -10,7 +10,7 @@ public class ForceClosedChannel extends ClosedChannel {
|
||||
ChannelCoreInformation channelCoreInformation,
|
||||
Pubkey ownPubkey,
|
||||
Pubkey remotePubkey,
|
||||
String closeTransactionHash,
|
||||
TransactionHash closeTransactionHash,
|
||||
OpenInitiator openInitiator,
|
||||
CloseInitiator closeInitiator,
|
||||
int closeHeight,
|
||||
|
||||
@@ -12,7 +12,7 @@ public final class ForceClosingChannel extends ClosedOrClosingChannel {
|
||||
ChannelCoreInformation channelCoreInformation,
|
||||
Pubkey ownPubkey,
|
||||
Pubkey remotePubkey,
|
||||
String closeTransactionHash,
|
||||
TransactionHash closeTransactionHash,
|
||||
Set<ChannelPoint> htlcOutpoints,
|
||||
OpenInitiator openInitiator
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package de.cotto.lndmanagej.model;
|
||||
|
||||
public interface OpenInitiatorResolver {
|
||||
OpenInitiator resolveFromOpenTransactionHash(String transactionHash);
|
||||
OpenInitiator resolveFromOpenTransactionHash(TransactionHash transactionHash);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.cotto.lndmanagej.model;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record Resolution(Optional<String> sweepTransaction, String resolutionType, String outcome) {
|
||||
public record Resolution(Optional<TransactionHash> sweepTransaction, String resolutionType, String outcome) {
|
||||
|
||||
private static final String FIRST_STAGE = "FIRST_STAGE";
|
||||
private static final String TIMEOUT = "TIMEOUT";
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.cotto.lndmanagej.model;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class TransactionHash {
|
||||
private static final Pattern PATTERN = Pattern.compile("[0-9a-fA-F]{64}");
|
||||
private final String hash;
|
||||
|
||||
private TransactionHash(String hash) {
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
public static TransactionHash create(String string) {
|
||||
if (!PATTERN.matcher(string).matches()) {
|
||||
throw new IllegalArgumentException("Transaction hash must have 64 hex characters");
|
||||
}
|
||||
return new TransactionHash(string.toLowerCase(Locale.US));
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || getClass() != other.getClass()) {
|
||||
return false;
|
||||
}
|
||||
TransactionHash that = (TransactionHash) other;
|
||||
return Objects.equals(hash, that.hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.cotto.lndmanagej.model;
|
||||
|
||||
import nl.jqno.equalsverifier.EqualsVerifier;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
class TransactionHashTest {
|
||||
|
||||
private static final String VALID_HASH = "abc222abc000abc000abc000abc000abc000abc000abc000abc000abc000abc0";
|
||||
|
||||
@Test
|
||||
void create_not_hex() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> TransactionHash.create("abc222abc000abc000abc000abc000abc000abc000abc000abc000abc000abcx")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_wrong_length() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> TransactionHash.create("abc222abc000abc000abc000abc000abc000abc000abc000abc000abc000abc00")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
assertThat(TransactionHash.create(VALID_HASH).getHash()).isEqualTo(VALID_HASH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToString() {
|
||||
assertThat(TransactionHash.create(VALID_HASH)).hasToString(VALID_HASH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEquals() {
|
||||
EqualsVerifier.forClass(TransactionHash.class).usingGetClass().verify();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package de.cotto.lndmanagej.model;
|
||||
|
||||
public class ChannelPointFixtures {
|
||||
public static final String TRANSACTION_HASH = "abc000abc000abc000abc000abc000abc000abc000abc000abc000abc000abc0";
|
||||
public static final String TRANSACTION_HASH_2 = "abc111abc000abc000abc000abc000abc000abc000abc000abc000abc000abc0";
|
||||
public static final String TRANSACTION_HASH_3 = "abc222abc000abc000abc000abc000abc000abc000abc000abc000abc000abc0";
|
||||
public static final TransactionHash TRANSACTION_HASH =
|
||||
TransactionHash.create("abc000abc000abc000abc000abc000abc000abc000abc000abc000abc000abc0");
|
||||
public static final TransactionHash TRANSACTION_HASH_2 =
|
||||
TransactionHash.create("abc111abc000abc000abc000abc000abc000abc000abc000abc000abc000abc0");
|
||||
public static final TransactionHash TRANSACTION_HASH_3 =
|
||||
TransactionHash.create("abc222abc000abc000abc000abc000abc000abc000abc000abc000abc000abc0");
|
||||
public static final int OUTPUT = 1;
|
||||
public static final int OUTPUT_2 = 123;
|
||||
public static final ChannelPoint CHANNEL_POINT = ChannelPoint.create(TRANSACTION_HASH + ":" + OUTPUT);
|
||||
|
||||
@@ -5,6 +5,7 @@ include 'caching'
|
||||
include 'forwarding-history'
|
||||
include 'grpc-adapter'
|
||||
include 'grpc-client'
|
||||
include 'hardcoded'
|
||||
include 'invoices'
|
||||
include 'model'
|
||||
include 'payments'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package de.cotto.lndmanagej.transactions;
|
||||
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.model.Transaction;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface TransactionDao {
|
||||
Optional<Transaction> getTransaction(String transactionHash);
|
||||
Optional<Transaction> getTransaction(TransactionHash transactionHash);
|
||||
|
||||
void saveTransaction(Transaction transaction);
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@JsonDeserialize(using = BitapsTransactionDto.Deserializer.class)
|
||||
public class BitapsTransactionDto extends TransactionDto {
|
||||
public BitapsTransactionDto(String hash, int blockHeight, int positionInBlock, Coins fees) {
|
||||
public BitapsTransactionDto(TransactionHash hash, int blockHeight, int positionInBlock, Coins fees) {
|
||||
super(hash, blockHeight, positionInBlock, fees);
|
||||
}
|
||||
|
||||
@@ -22,11 +23,11 @@ public class BitapsTransactionDto extends TransactionDto {
|
||||
DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
JsonNode transactionDetailsNode = jsonParser.getCodec().<JsonNode>readTree(jsonParser).get("data");
|
||||
String hash = transactionDetailsNode.get("txId").textValue();
|
||||
TransactionHash hash = TransactionHash.create(transactionDetailsNode.get("txId").textValue());
|
||||
int blockHeight = transactionDetailsNode.get("blockHeight").asInt();
|
||||
long fees = transactionDetailsNode.get("fee").asLong();
|
||||
Coins fees = Coins.ofSatoshis(transactionDetailsNode.get("fee").asLong());
|
||||
int positionInBlock = transactionDetailsNode.get("blockIndex").asInt();
|
||||
return new BitapsTransactionDto(hash, blockHeight, positionInBlock, Coins.ofSatoshis(fees));
|
||||
return new BitapsTransactionDto(hash, blockHeight, positionInBlock, fees);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@JsonDeserialize(using = BlockcypherTransactionDto.Deserializer.class)
|
||||
public final class BlockcypherTransactionDto extends TransactionDto {
|
||||
public BlockcypherTransactionDto(String hash, int blockHeight, int positionInBlock, Coins fees) {
|
||||
public BlockcypherTransactionDto(TransactionHash hash, int blockHeight, int positionInBlock, Coins fees) {
|
||||
super(hash, blockHeight, positionInBlock, fees);
|
||||
}
|
||||
|
||||
@@ -22,11 +23,11 @@ public final class BlockcypherTransactionDto extends TransactionDto {
|
||||
DeserializationContext context
|
||||
) throws IOException {
|
||||
JsonNode transactionDetailsNode = jsonParser.getCodec().readTree(jsonParser);
|
||||
String hash = transactionDetailsNode.get("hash").textValue();
|
||||
TransactionHash hash = TransactionHash.create(transactionDetailsNode.get("hash").textValue());
|
||||
int blockHeight = transactionDetailsNode.get("block_height").asInt();
|
||||
long fees = transactionDetailsNode.get("fees").asLong();
|
||||
Coins fees2 = Coins.ofSatoshis(transactionDetailsNode.get("fees").asLong());
|
||||
int positionInBlock = transactionDetailsNode.get("block_index").asInt();
|
||||
return new BlockcypherTransactionDto(hash, blockHeight, positionInBlock, Coins.ofSatoshis(fees));
|
||||
return new BlockcypherTransactionDto(hash, blockHeight, positionInBlock, fees2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package de.cotto.lndmanagej.transactions.download;
|
||||
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.model.Transaction;
|
||||
|
||||
public class TransactionDto {
|
||||
private final String hash;
|
||||
private final TransactionHash hash;
|
||||
private final int blockHeight;
|
||||
private final int positionInBlock;
|
||||
private final Coins fees;
|
||||
|
||||
public TransactionDto(
|
||||
String hash,
|
||||
TransactionHash hash,
|
||||
int blockHeight,
|
||||
int positionInBlock,
|
||||
Coins fees
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.cotto.lndmanagej.transactions.download;
|
||||
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.model.Transaction;
|
||||
import feign.FeignException;
|
||||
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
||||
@@ -21,11 +22,11 @@ public class TransactionProvider {
|
||||
this.clients = clients;
|
||||
}
|
||||
|
||||
public Optional<Transaction> get(String transactionHash) {
|
||||
public Optional<Transaction> get(TransactionHash transactionHash) {
|
||||
return getAndHandleExceptions(transactionHash).map(TransactionDto::toModel);
|
||||
}
|
||||
|
||||
private Optional<? extends TransactionDto> getAndHandleExceptions(String transactionHash) {
|
||||
private Optional<? extends TransactionDto> getAndHandleExceptions(TransactionHash transactionHash) {
|
||||
List<TransactionDetailsClient> randomizedClients = new ArrayList<>(clients);
|
||||
Collections.shuffle(randomizedClients);
|
||||
return randomizedClients.stream()
|
||||
@@ -34,9 +35,12 @@ public class TransactionProvider {
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private Optional<? extends TransactionDto> getWithClient(String transactionHash, TransactionDetailsClient client) {
|
||||
private Optional<? extends TransactionDto> getWithClient(
|
||||
TransactionHash transactionHash,
|
||||
TransactionDetailsClient client
|
||||
) {
|
||||
try {
|
||||
return client.getTransaction(transactionHash);
|
||||
return client.getTransaction(transactionHash.getHash());
|
||||
} catch (FeignException feignException) {
|
||||
logger.warn("Feign exception: ", feignException);
|
||||
return Optional.empty();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package de.cotto.lndmanagej.transactions.model;
|
||||
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
|
||||
public record Transaction(
|
||||
String hash,
|
||||
TransactionHash hash,
|
||||
int blockHeight,
|
||||
int positionInBlock,
|
||||
Coins fees
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.cotto.lndmanagej.transactions.persistence;
|
||||
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.TransactionDao;
|
||||
import de.cotto.lndmanagej.transactions.model.Transaction;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -17,8 +18,8 @@ public class TransactionDaoImpl implements TransactionDao {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Transaction> getTransaction(String transactionHash) {
|
||||
return transactionRepository.findById(transactionHash)
|
||||
public Optional<Transaction> getTransaction(TransactionHash transactionHash) {
|
||||
return transactionRepository.findById(transactionHash.getHash())
|
||||
.flatMap(TransactionJpaDto::toModel);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.cotto.lndmanagej.transactions.persistence;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.model.Transaction;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
@@ -31,7 +32,7 @@ class TransactionJpaDto {
|
||||
|
||||
protected static TransactionJpaDto fromModel(Transaction transaction) {
|
||||
TransactionJpaDto dto = new TransactionJpaDto();
|
||||
dto.setHash(transaction.hash());
|
||||
dto.setHash(transaction.hash().getHash());
|
||||
dto.setBlockHeight(transaction.blockHeight());
|
||||
dto.setFees(transaction.fees().satoshis());
|
||||
dto.setPositionInBlock(transaction.positionInBlock());
|
||||
@@ -43,7 +44,7 @@ class TransactionJpaDto {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new Transaction(
|
||||
hash,
|
||||
TransactionHash.create(hash),
|
||||
blockHeight,
|
||||
positionInBlock,
|
||||
Coins.ofSatoshis(fees)
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.cotto.lndmanagej.transactions.service;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import de.cotto.lndmanagej.caching.CacheBuilder;
|
||||
import de.cotto.lndmanagej.grpc.GrpcTransactions;
|
||||
import de.cotto.lndmanagej.model.TransactionHash;
|
||||
import de.cotto.lndmanagej.transactions.TransactionDao;
|
||||
import de.cotto.lndmanagej.transactions.download.TransactionProvider;
|
||||
import de.cotto.lndmanagej.transactions.model.Transaction;
|
||||
@@ -18,7 +19,7 @@ public class TransactionService {
|
||||
private final TransactionDao transactionDao;
|
||||
private final TransactionProvider transactionProvider;
|
||||
private final GrpcTransactions grpcTransactions;
|
||||
private final LoadingCache<String, Optional<Boolean>> hashIsKnownCache;
|
||||
private final LoadingCache<TransactionHash, Optional<Boolean>> hashIsKnownCache;
|
||||
|
||||
public TransactionService(
|
||||
TransactionDao transactionDao,
|
||||
@@ -35,18 +36,18 @@ public class TransactionService {
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.LinguisticNaming")
|
||||
public Optional<Boolean> isKnownByLnd(String transactionHash) {
|
||||
public Optional<Boolean> isKnownByLnd(TransactionHash transactionHash) {
|
||||
return hashIsKnownCache.get(transactionHash);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.LinguisticNaming")
|
||||
public Optional<Boolean> isKnownByLndWithoutCache(String transactionHash) {
|
||||
public Optional<Boolean> isKnownByLndWithoutCache(TransactionHash transactionHash) {
|
||||
Transaction transaction = getTransaction(transactionHash).orElse(null);
|
||||
if (transaction == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
int blockHeight = transaction.blockHeight();
|
||||
Set<String> knownTransactionsInBlock = grpcTransactions.getKnownTransactionHashesInBlock(blockHeight)
|
||||
Set<TransactionHash> knownTransactionsInBlock = grpcTransactions.getKnownTransactionHashesInBlock(blockHeight)
|
||||
.orElse(null);
|
||||
if (knownTransactionsInBlock == null) {
|
||||
return Optional.empty();
|
||||
@@ -54,7 +55,7 @@ public class TransactionService {
|
||||
return Optional.of(knownTransactionsInBlock.contains(transactionHash));
|
||||
}
|
||||
|
||||
public Optional<Transaction> getTransaction(String transactionHash) {
|
||||
public Optional<Transaction> getTransaction(TransactionHash transactionHash) {
|
||||
Optional<Transaction> persistedTransaction = transactionDao.getTransaction(transactionHash);
|
||||
if (persistedTransaction.isPresent()) {
|
||||
return persistedTransaction;
|
||||
@@ -62,15 +63,15 @@ public class TransactionService {
|
||||
return downloadAndPersist(transactionHash);
|
||||
}
|
||||
|
||||
public boolean isKnown(String transactionHash) {
|
||||
public boolean isKnown(TransactionHash transactionHash) {
|
||||
return transactionDao.getTransaction(transactionHash).isPresent();
|
||||
}
|
||||
|
||||
public boolean isUnknown(String transactionHash) {
|
||||
public boolean isUnknown(TransactionHash transactionHash) {
|
||||
return !isKnown(transactionHash);
|
||||
}
|
||||
|
||||
private Optional<Transaction> downloadAndPersist(String transactionHash) {
|
||||
private Optional<Transaction> downloadAndPersist(TransactionHash transactionHash) {
|
||||
Optional<Transaction> optionalTransaction = transactionProvider.get(transactionHash);
|
||||
optionalTransaction.ifPresent(transactionDao::saveTransaction);
|
||||
return optionalTransaction;
|
||||
|
||||
@@ -55,13 +55,14 @@ class TransactionProviderTest {
|
||||
|
||||
@Test
|
||||
void success_first_client() {
|
||||
when(blockcypherClient.getTransaction(TRANSACTION_HASH)).thenReturn(Optional.of(BLOCKCYPHER_TRANSACTION));
|
||||
when(blockcypherClient.getTransaction(TRANSACTION_HASH.getHash()))
|
||||
.thenReturn(Optional.of(BLOCKCYPHER_TRANSACTION));
|
||||
assertThat(transactionProvider.get(TRANSACTION_HASH)).contains(TRANSACTION);
|
||||
}
|
||||
|
||||
@Test
|
||||
void success_second_client() {
|
||||
when(bitapsClient.getTransaction(TRANSACTION_HASH)).thenReturn(Optional.of(BITAPS_TRANSACTION));
|
||||
when(bitapsClient.getTransaction(TRANSACTION_HASH.getHash())).thenReturn(Optional.of(BITAPS_TRANSACTION));
|
||||
assertThat(transactionProvider.get(TRANSACTION_HASH)).contains(TRANSACTION);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class TransactionDaoImplTest {
|
||||
|
||||
@Test
|
||||
void getTransaction() {
|
||||
when(transactionRepository.findById(TRANSACTION_HASH)).thenReturn(Optional.of(TRANSACTION_JPA_DTO));
|
||||
when(transactionRepository.findById(TRANSACTION_HASH.getHash())).thenReturn(Optional.of(TRANSACTION_JPA_DTO));
|
||||
|
||||
assertThat(transactionDao.getTransaction(TRANSACTION_HASH)).contains(TRANSACTION);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class TransactionDaoImplTest {
|
||||
@Test
|
||||
void saveTransaction() {
|
||||
transactionDao.saveTransaction(TRANSACTION);
|
||||
verify(transactionRepository).save(argThat(dto -> TRANSACTION_HASH.equals(dto.getHash())));
|
||||
verify(transactionRepository).save(argThat(dto -> TRANSACTION_HASH.getHash().equals(dto.getHash())));
|
||||
verify(transactionRepository).save(argThat(dto -> BLOCK_HEIGHT == dto.getBlockHeight()));
|
||||
verify(transactionRepository).save(argThat(dto -> POSITION_IN_BLOCK == dto.getPositionInBlock()));
|
||||
verify(transactionRepository).save(argThat(dto -> FEES.satoshis() == dto.getFees()));
|
||||
|
||||
@@ -10,7 +10,7 @@ public class TransactionJpaDtoFixtures {
|
||||
|
||||
static {
|
||||
TRANSACTION_JPA_DTO = new TransactionJpaDto();
|
||||
TRANSACTION_JPA_DTO.setHash(TRANSACTION_HASH);
|
||||
TRANSACTION_JPA_DTO.setHash(TRANSACTION_HASH.getHash());
|
||||
TRANSACTION_JPA_DTO.setBlockHeight(BLOCK_HEIGHT);
|
||||
TRANSACTION_JPA_DTO.setPositionInBlock(POSITION_IN_BLOCK);
|
||||
TRANSACTION_JPA_DTO.setFees(FEES.satoshis());
|
||||
|
||||
Reference in New Issue
Block a user