allow users to specify resolutions manually

This commit is contained in:
Carsten Otto
2021-12-12 21:02:17 +01:00
parent 848afefc84
commit 67e8bc6637
44 changed files with 541 additions and 100 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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);

View File

@@ -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'))
}

View File

@@ -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())
));

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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()

View File

@@ -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
View 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'))
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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");
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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
) {

View File

@@ -1,5 +1,5 @@
package de.cotto.lndmanagej.model;
public interface OpenInitiatorResolver {
OpenInitiator resolveFromOpenTransactionHash(String transactionHash);
OpenInitiator resolveFromOpenTransactionHash(TransactionHash transactionHash);
}

View File

@@ -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";

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -5,6 +5,7 @@ include 'caching'
include 'forwarding-history'
include 'grpc-adapter'
include 'grpc-client'
include 'hardcoded'
include 'invoices'
include 'model'
include 'payments'

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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()));

View File

@@ -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());