Skip to content

Commit

Permalink
add support for lnd sweep transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
C-Otto committed Apr 14, 2021
1 parent bbab1da commit d247fc5
Show file tree
Hide file tree
Showing 13 changed files with 479 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ Please have a look at the [Frequently Asked Questions (FAQ)](documentation/faq.m
* [Feature Requests and Bug Reports](documentation/features_and_bugs.md)
* [Known Limitations](documentation/limitations.md)
* [Future Ideas](documentation/ideas.md)
* [LND support](documentation/lnd.md)
* [Technical Aspects](documentation/technical.md)
* [Contributing to BitBook](documentation/contributing.md)
3 changes: 2 additions & 1 deletion cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies {
implementation project(':backend-transaction-providers')
implementation project(':backend')
implementation project(':ownership-cli')
implementation project(':lnd-cli')
implementation project(':cli-base')
implementation 'org.flywaydb:flyway-core'
testImplementation testFixtures(project(':backend-transaction'))
Expand All @@ -19,4 +20,4 @@ bootJar {
}
bootRun {
standardInput = System.in
}
}
5 changes: 4 additions & 1 deletion documentation/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,7 @@ BitBook$ get-address-transactions 1ET8va8cJNGGLtG7pwRq79EeE7qNb7ofCS Pete Peters

The list of completion candidates is adapted to the needs of the specific commands.
As an example, when using `remove-transaction-description`, you are only shown transaction hashes for which a
description is known.
description is known.

### LND Support
Please have a look at [lnd.md](lnd.md).
33 changes: 33 additions & 0 deletions documentation/lnd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# LND Support
BitBook has basic support for [LND (lightning network daemon)](https://github.com/lightningnetwork/lnd) so that you can
add ownership information and helpful descriptions for transactions/addresses managed by your LND instance without
entering these manually.

This is implemented by parsing JSON files generated by the `lncli` command line tool. As such, this involves a bit of
manual effort, but it also keeps things simple. By copying these files youc an easily have BitBook and lnd run on
different computers. Furthermore, by not giving BitBook access to lnd, there's no risk of undesired side effects.

### Sweeps
After a channel is closed, your funds (on the "local" side of the channel) are sent to an address that is not derived
from your lnd wallet seed. To avoid loss of funds, lnd automatically "sweeps" those funds to another address. As such,
there may be several sweep transactions that transfer funds from one address to another.

BitBook offers the command `lnd-add-from-sweeps` which parses lnd sweep information and does the following for each
transaction:

- sets the transaction description to "LND sweep transaction"
- sets the target address description to "LND"
- sets the source address description to "LND" if it is not set
(a better description can be obtained when parsing channel close data)
- marks both addresses as owned

To run the command:
1. first create the JSON file using lnd: `$ lncli wallet listsweeps > lnd-sweeps.json`
2. transfer the JSON file to the host where you are running BitBook: `$ scp server:/home/lnd/lnd-sweeps.json /tmp/`
3. start BitBook

Then you can use the command as follows:
```
BitBook$ lnd-add-from-sweeps /tmp/lnd-sweeps.json
Added information for 86 sweep transactions
```
8 changes: 8 additions & 0 deletions lnd-cli/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
id 'bitbook.java-library-conventions'
}

dependencies {
implementation project(':cli-base')
implementation project(':lnd')
}
29 changes: 29 additions & 0 deletions lnd-cli/src/main/java/de/cotto/bitbook/lnd/cli/LndCommands.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package de.cotto.bitbook.lnd.cli;

import de.cotto.bitbook.lnd.LndService;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;

@ShellComponent
public class LndCommands {
private final LndService lndService;

public LndCommands(LndService lndService) {
this.lndService = lndService;
}

@ShellMethod("Add information from lnd sweep information obtained by `lncli wallet listsweeps`")
public String lndAddFromSweeps(File jsonFile) throws IOException {
String content = Files.readString(jsonFile.toPath(), StandardCharsets.US_ASCII);
long numberOfSweepTransactions = lndService.lndAddFromSweeps(content);
if (numberOfSweepTransactions == 0) {
return "Unable to find sweep transactions in file";
}
return "Added information for " + numberOfSweepTransactions + " sweep transactions";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package de.cotto.bitbook.lnd.cli;

import de.cotto.bitbook.lnd.LndService;
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.io.File;
import java.io.IOException;
import java.nio.file.Files;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class LndCommandsTest {
@InjectMocks
private LndCommands lndCommands;

@Mock
private LndService lndService;

@Test
void lndAddFromSweeps() throws IOException {
when(lndService.lndAddFromSweeps(any())).thenReturn(123L);
String json = "{\"foo\": \"bar\"}";
File file = File.createTempFile("temp", "bitbook");
Files.writeString(file.toPath(), json);

assertThat(lndCommands.lndAddFromSweeps(file)).isEqualTo("Added information for 123 sweep transactions");

verify(lndService).lndAddFromSweeps(json);
}

@Test
void lndAddFromSweeps_failure() throws IOException {
when(lndService.lndAddFromSweeps(any())).thenReturn(0L);
File file = File.createTempFile("temp", "bitbook");

assertThat(lndCommands.lndAddFromSweeps(file)).isEqualTo("Unable to find sweep transactions in file");
}
}
10 changes: 10 additions & 0 deletions lnd/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
plugins {
id 'bitbook.java-library-conventions'
id 'java-test-fixtures'
}

dependencies {
implementation project(':backend-transaction')
implementation project(':ownership')
testImplementation testFixtures(project(':backend-transaction-models'))
}
132 changes: 132 additions & 0 deletions lnd/src/main/java/de/cotto/bitbook/lnd/LndService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package de.cotto.bitbook.lnd;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.cotto.bitbook.backend.AddressDescriptionService;
import de.cotto.bitbook.backend.TransactionDescriptionService;
import de.cotto.bitbook.backend.model.AddressWithDescription;
import de.cotto.bitbook.backend.transaction.TransactionService;
import de.cotto.bitbook.backend.transaction.model.Transaction;
import de.cotto.bitbook.ownership.AddressOwnershipService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Nullable;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;

@Component
public class LndService {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final ObjectMapper objectMapper;
private final TransactionService transactionService;
private final TransactionDescriptionService transactionDescriptionService;
private final AddressDescriptionService addressDescriptionService;
private final AddressOwnershipService addressOwnershipService;

public LndService(
TransactionService transactionService,
AddressDescriptionService addressDescriptionService,
TransactionDescriptionService transactionDescriptionService,
AddressOwnershipService addressOwnershipService,
ObjectMapper objectMapper
) {
this.transactionService = transactionService;
this.addressDescriptionService = addressDescriptionService;
this.transactionDescriptionService = transactionDescriptionService;
this.addressOwnershipService = addressOwnershipService;
this.objectMapper = objectMapper;
}

public long lndAddFromSweeps(String json) {
Set<String> hashes = parseHashes(json);
return hashes.stream()
.map(this::getGetTransactionDetails)
.filter(this::isSweepTransaction)
.map(this::addTransactionDescription)
.map(this::addAddressDescriptions)
.map(this::setAddressesAsOwned)
.count();
}

private Set<String> parseHashes(String json) {
try (JsonParser parser = objectMapper.createParser(json)) {
JsonNode rootNode = parser.getCodec().readTree(parser);
return parseHashes(rootNode);
} catch (IOException e) {
return Set.of();
}
}

private Set<String> parseHashes(@Nullable JsonNode rootNode) {
if (rootNode == null) {
return Set.of();
}
JsonNode sweeps = rootNode.get("Sweeps");
if (sweeps == null) {
return Set.of();
}
JsonNode transactionIds = sweeps.get("TransactionIds");
if (transactionIds == null) {
return Set.of();
}
JsonNode hashesArray = transactionIds.get("transaction_ids");
if (hashesArray == null) {
return Set.of();
}
Set<String> hashes = new LinkedHashSet<>();
for (JsonNode hash : hashesArray) {
hashes.add(hash.textValue());
}
return hashes;
}

private Transaction getGetTransactionDetails(String transactionHash) {
Transaction transactionDetails = transactionService.getTransactionDetails(transactionHash);
if (transactionDetails.isInvalid()) {
logger.warn("Unable to find transaction {}", transactionHash);
}
return transactionDetails;
}

private boolean isSweepTransaction(Transaction transaction) {
boolean hasSingleOutput = transaction.getOutputs().size() == 1;
if (!hasSingleOutput && !transaction.equals(Transaction.UNKNOWN)) {
logger.warn("Not a sweep transaction: {}", transaction);
}
return hasSingleOutput;
}

private Transaction addTransactionDescription(Transaction transaction) {
transactionDescriptionService.set(transaction.getHash(), "LND sweep transaction");
return transaction;
}

private Transaction addAddressDescriptions(Transaction transaction) {
String inputAddress = getInputAddress(transaction);
AddressWithDescription addressWithDescription = addressDescriptionService.get(inputAddress);
if (addressWithDescription.getDescription().isBlank()) {
addressDescriptionService.set(inputAddress, "LND");
}
addressDescriptionService.set(getOutputAddress(transaction), "LND");
return transaction;
}

@SuppressWarnings("PMD.LinguisticNaming")
private Transaction setAddressesAsOwned(Transaction transaction) {
addressOwnershipService.setAddressAsOwned(getInputAddress(transaction));
addressOwnershipService.setAddressAsOwned(getOutputAddress(transaction));
return transaction;
}

private String getOutputAddress(Transaction transaction) {
return transaction.getOutputs().get(0).getAddress();
}

private String getInputAddress(Transaction transaction) {
return transaction.getInputs().get(0).getAddress();
}
}
Loading

0 comments on commit d247fc5

Please sign in to comment.