Lightning APIs for Java V 0.5.0-Beta, 2018-09-14

Introduction

LightningJ is a project with the intention to simplify the integration of existing Lightning node implementations for Java developers. It contains simple to use API implementations and converters between JSON, XML.

More information and source code can be found on the Github site. There it is possible to report issues and contribute code.

If you are developing using the grails framework there is also a Grails 3.3+ plugin at https://grails.lightningj.org

If you want updates on this project, follow https://twitter.com/LightningJ_org on twitter.

Important: This API is still in beta and API methods might change before real production release.
All use of this library is on your own risk.

What LightningJ is Not

LightningJ is not itself a Lightning Node implementation but contains APIs to easy integrate existing Lightning (currently LND) implementations using Java.

License

This library is Open Source and released under the LGPL v3 License. A link to the license agreement can be found here.

Whats New

  • 0.5.0-Beta : Generated API from LND 0.5.0 Beta release.

  • 0.5.0-Beta-rc2 : Generated API from LND 0.5.0 Beta RC2 release.

  • 0.5.0-Beta-rc1 : Generated API from LND 0.5.0 Beta RC1 release.

  • 0.4.2-Beta-2 : Bugfix for null pointer problem generating invoices and other messages containing unset repeatable fields.

  • 0.4.2-Beta : Generated API from LND 0.4.2 Beta release.

  • 0.4.1-Beta : Generated API from LND 0.4.1 Beta release.

  • 0.4-Beta : API generated from LDN rpc.proto for LND 0.4-Beta tag. Also check out the new grails plugin as https://grails.lightningj.org

  • 0.3-Beta : Z-Base32 encoder/decoder, updated API to support new Wallet Seed generation.

  • 0.2-Beta : Added support for Macaroons authentication.

  • 0.1-Beta : This is the initial release with generated APIs (Synchronous and Asynchronous) for LND.

Roadmap

  • LND: Keep rpc.proto specification updated with latest LND release.

  • Implement equivalent API for Lightning-c and or Eclair

Using LightningJ Library

To use this library you can either add it as a dependency from maven central repository or build it from source.

From Maven Central

Add the following dependency to your pom.xml

   <dependency>
      <groupId>org.lightningj</groupId>
      <artifactId>lightningj</artifactId>
      <version>0.5.0-Beta</version>
   </dependency>

Or to your build.gradle

    compile 'org.lightningj:lightningj:0.5.0-Beta'

All tags and releases is signed with the following GPG Key.

GPG Key Fingerprint:

7C0F 80B8 BD9F E3B8 1388  4BA1 9515 B31D DD9B BCCD

From Source

To build from source clone the repository and use gradlew to build.

git clone https://github.com/lightningj-org/lightningj.git
cd lightningj
./gradlew build

The generated jars is located in build/libs.

Using the LND API

This section contains information on how to use the APIs to connect and communicate with a LND node.

The LightningJ takes the LND GRPC (gRPC Remote Procedure Calls) proto specification file (rpc.proto) and first generates the low-level GRPC API using standard GRPC Java. Then it generates a wrapping high level API and adds JSON, XML and validation features on top of the underlying GRPC message objects.

In the source there is a directory src/examples/lnd that also contains tips and tricks on how to use the API.

Getting started with LND

To get started with a LND node, see the LND developer site: http://dev.lightning.community/. There is an installation guide and a tutorial.

Using the High Level API

The high level api contains wrapper classes and a API interface for both synchronous and asynchronous calls. There is two APIs generated, the main LND API and Wallet Unlocker API.

When creating an instance it is possible to either specify the trusted SSL Certificate and the macaroon file that should be used. (If no macaroon is required by the LND node is null acceptable as parameter). Or specify a custom SSL Context and Macaroon Context for more advanced control.

For more details about each call see LND API documentation

Synchronous API

The synchronous APIs are SynchronousLndAPI and SynchronousWalletUnlockerAPI that waits for response before continuing the thread.

Below is an example on how to use a Synchronous API.

// To create a synchronousAPI there are three constructors available
// One simple with host,port and certificate to trust, last file is the file path to the macaroon, use null if no macaroons are used.
SynchronousLndAPI synchronousLndAPI = new SynchronousLndAPI("localhost",10001,
new File("/Library/Application Support/Lnd/tls.cert"),
new File(System.getProperty("user.home")+ "/Library/Application Support/Lnd/admin.macaroon"));
// A second with host,port and a custom SSL Context for more advanced SSL Context and Macaroon Context settings.
//SynchronousLndAPI synchronousLndAPI = new SynchronousLndAPI("localhost",10001,sSLContext, macaroonContext);
// The third that takes a ManagedChannel, with full customization capabilities of underlying API
// See GRPC Java documentation for details.
//SynchronousLndAPI synchronousLndAPI = new SynchronousLndAPI(managedChannel);

// By default is validation performed on all inbound and outbound messages, to turn of validation:
//synchronousLndAPI.setPerformValidation(false);

// Example call to get channel balance and write output as JSON (pretty printed)
System.out.println(synchronousLndAPI.channelBalance().toJsonAsString(true));

// Calls returns a wrapped response or Iterator of wrapped responses.
// Example to get a response:
ListPeersResponse listPeersResponse = synchronousLndAPI.listPeers();
// The response can be converted to XML or JSON or just parsed.


// A more advanced call returning an iterator is for example openChannel().

// To generate a request call, there are two ways to generate a request.
// Either build up a request object like below:
OpenChannelRequest openChannelRequest = new OpenChannelRequest();
openChannelRequest.setNodePubkeyString("02ad1fddad0c572ec3e886cbea31bbafa30b5f7e745da7e936ed9d1471116cdc02");
openChannelRequest.setLocalFundingAmount(40000);
openChannelRequest.setPushSat(25000);
openChannelRequest.setSatPerByte(0);

// Alternatively it is possible to specify the parameters directly without having to create a request.
// Iterator<OpenStatusUpdate> result = synchronousLndAPI.openChannel(1,null,"02ad1fddad0c572ec3e886cbea31bbafa30b5f7e745da7e936ed9d1471116cdc02", 40000L,25000L,null,0L,null,null);

// Perform the call using alternative 1
Iterator<OpenStatusUpdate> result = synchronousLndAPI.openChannel(openChannelRequest);

// This call will wait for a the channel has opened, which means confirmation block must
// generated in btc. If simnet is used you can manually generate blocks with
// 'btcctl --simnet --rpcuser=kek --rpcpass=kek generate 3'

while(result.hasNext()){
    System.out.println("Received Update: " + result.next().toJsonAsString(true));
}

// To close the api use the method
synchronousLndAPI.close();

Asynchronous API

The asynchronous is a non-blocking API that doesn’t wait for a response but expects a StreamObserver implementation handling the response at a later time and is useful i GUI applications to give a more fluent experience.

The variants of Asynchronous APIs are AsynchronousLndAPI and AsynchronousWalletUnlockerAPI.

And example on how to use the Asynchronous API

// Create  API, using the most simple constructor. There are alternatives
// where it is possible to specify custom SSLContext or just a managed channel.
// See SynchronousLndAPIExample for details.
AsynchronousLndAPI asynchronousLndAPI = new AsynchronousLndAPI("localhost",10001,new File("/Users/philip/Library/Application Support/Lnd/tls.cert"), null);

try {
    // Example of a simple asynchronous call.
    System.out.println("Sending WalletBalance request...");
    asynchronousLndAPI.walletBalance(new StreamObserver<WalletBalanceResponse>() {

        // Each response is sent in a onNext call.
        @Override
        public void onNext(WalletBalanceResponse value) {
            System.out.println("Received WalletBalance response: " + value.toJsonAsString(true));
        }

        // Errors during the stream is showed here.
        @Override
        public void onError(Throwable t) {
            System.err.println("Error occurred during WalletBalance call: " + t.getMessage());
            t.printStackTrace(System.err);
        }

        // When the stream have finished is onCompleted called.
        @Override
        public void onCompleted() {
            System.out.println("WalletBalance call closed.");
        }
    });

    // Call to subscribe for invoices.
    // To recieve invoices you can use the lncli to send payment of an invoice to your LND node.
    // and it will show up here.
    System.out.println("Subscribing to invoices call...");
    asynchronousLndAPI.subscribeInvoices(new StreamObserver<Invoice>() {
        @Override
        public void onNext(Invoice value) {
            System.out.println("Received Invoice: " + value.toJsonAsString(true));
        }

        @Override
        public void onError(Throwable t) {
            System.err.println("Error occurred during subscribeInvoices call: " + t.getMessage());
            t.printStackTrace(System.err);
        }

        @Override
        public void onCompleted() {
            System.out.println("subscribeInvoices call closed.");
        }
    });

    System.out.println("Press Ctrl-C to stop listening for invoices");
    while (true) {
        Thread.sleep(1000);
    }

}finally {
    // To close the api use the method
    asynchronousLndAPI.close();
}

Json Conversion

The libarary uses the JSR 374 javax.json api to generate and parse JSON.

To convert between JSON and High Level API object is pretty straight forward as shown in following example:

// Get API
SynchronousLndAPI synchronousLndAPI = getSynchronousLndAPI();

// To convert JSON request data to a wrapped request object (High level)
// Do the following
String jsonData = "{\"node_pubkey\":\"\",\"node_pubkey_string\":\"02ad1fddad0c572ec3e886cbea31bbafa30b5f7e745da7e936ed9d1471116cdc02\",\"local_funding_amount\":40000,\"push_sat\":25000,\"targetConf\":0,\"satPerByte\":0,\"private\":false,\"min_htlc_msat\":0}";

// The library uses the javax.json-api 1.0 (JSR 374) API to parse and generate JSON.
// To parse a JSON String, start by creating a JsonReader
JsonReader jsonReader = Json.createReader(new StringReader(jsonData));

// Then parse by creating a Wrapped Message object.
OpenChannelRequest openChannelRequest = new OpenChannelRequest(jsonReader);

// Perform the call.
Iterator<OpenStatusUpdate> result = synchronousLndAPI.openChannel(openChannelRequest);

// This call will wait for a the channel has opened, which means confirmation block must
// generated in btc. If simnet is used you can manually generate blocks with
// 'btcctl --simnet --rpcuser=kek --rpcpass=kek generate 3'

while(result.hasNext()){
    // To generate JSON from a response there are three possiblities, either
    OpenStatusUpdate next = result.next();
    // To get JSON as String
    System.out.println("Received Update: " + next.toJsonAsString(false));
    // To have the result more human readable set pretty print to true
    System.out.println("Received Update: " + next.toJsonAsString(true));
    // It is also possible to get the JSON as a populated JsonObjectBuilder
    JsonObjectBuilder jsonObjectBuilder = next.toJson();
}

XML Conversion

For XML parsing and generation is JAXB used. And to convert between XML data and high level wrapper object is a XMLParser used.

Use XMLParserFactory to retrieve a XMLParser for the used XML Schema version (currently only version "1.0" exist and should still not be considered final and could change.)

An example on XML conversion:

// Get API
SynchronousLndAPI synchronousLndAPI = getSynchronousLndAPI();

// Create a XMLParserFactory
XMLParserFactory xmlParserFactory = new XMLParserFactory();

// Retrieve XML Parser for a given XML version schema. (Currently "1.0")
XMLParser xmlParser = xmlParserFactory.getXMLParser("1.0");

byte[] xmlRequestData = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><OpenChannelRequest xmlns=\"http://lightningj.org/xsd/lndjapi_1_0\"><nodePubkey></nodePubkey><nodePubkeyString>02ad1fddad0c572ec3e886cbea31bbafa30b5f7e745da7e936ed9d1471116cdc02</nodePubkeyString><localFundingAmount>40000</localFundingAmount><pushSat>25000</pushSat><targetConf>0</targetConf><satPerByte>0</satPerByte><private>false</private><minHtlcMsat>0</minHtlcMsat></OpenChannelRequest>".getBytes("UTF-8");

// Convert to a wrapped high level message object.
OpenChannelRequest openChannelRequest = (OpenChannelRequest) xmlParser.unmarshall(xmlRequestData);

// Perform the call.
Iterator<OpenStatusUpdate> result = synchronousLndAPI.openChannel(openChannelRequest);

// This call will wait for a the channel has opened, which means confirmation block must
// generated in btc. If simnet is used you can manually generate blocks with
// 'btcctl --simnet --rpcuser=kek --rpcpass=kek generate 3'

while(result.hasNext()){
    // To generate XML from a response do the following:
    OpenStatusUpdate next = result.next();
    // To get XML as byte[]
    byte[] responseData = xmlParser.marshall(next);
    System.out.println("XML Response data: " + new String(responseData,"UTF-8"));
    // To get XML pretty printed
    byte[] responseDataPrettyPrinted = xmlParser.marshall(next,true);
    System.out.println("Pretty Printed XML Response data: Œ" + new String(responseDataPrettyPrinted,"UTF-8"));
}

The latest XSD schema can be found here lnd_v1.xsd

Validation

The library also have a validation functionality to validate messages. It uses the underlying proto specification to check that each field has accepted values. Currently there are not that many validation related parameters specified in the rpc.proto but might improve in the future that will make the validation parts of the library more useful.

Below is an example of how validation can be done:

// Get API
SynchronousLndAPI synchronousLndAPI = getSynchronousLndAPI();

// To manually validate a wrapped Message it is possible to call the validate() method.
OpenChannelRequest openChannelRequest = genOpenChannelRequest();
// To validate call validate() and it will return ValidationResult
ValidationResult validationResult = openChannelRequest.validate();
// The ValidationResult.isValid() returns true if the message was valud.
validationResult.isValid();
// If there is problems it is possible to retrieve the problems found either
// in a single aggregated list for all sub-messages.
List<ValidationProblems> allProblems= validationResult.getAggregatedValidationErrors();
// Or as a tree structure with all problems in this message in:
validationResult.getMessageErrors();
// and all sub messages as their own report.
validationResult.getSubMessageResults();


try{
    // Each call might throw a ValidationException
    synchronousLndAPI.channelBalance();
}catch(ValidationException ve){
    // A ValidationException has the faulty messages ValidationReport as a field.
    ValidationResult vr = ve.getValidationResult();
}catch(StatusException se){
    //...
}

Validation Internationalization

Each ValidationProblem has a translatable message resource key as a field. The message resource file bundle is in src/main/resources/lightningj_messages

Exception Handling

High Level API

The High Level API has two categories of exceptions that can be thrown during an API call. One is ValidationException indicating that a message didn’t conform to GRPC Proto specification. The other category consist of a base StatusException, (wrapping the low level io.grpc.StatusException or io.grpc.StatusRuntimeException), and three sub exception indicating the type of status problem that occurred and that could be handled differently.

Here is a list of status exceptions

Table 1. Types of Status Exceptions
Exception Description

StatusException

Base exception for all types of GRPC related problems.

ClientSideException

Indicate there is some problem on the client side such as invalid request data.

ServerSideException

Indicate there is some problem on the server side that might persist for some time.

CommunicationException

This could indicate timeout or dropped package and request can be retried.

So when calling an API call you can either choose to just handle ValidationException or StatusException or to do more fine pruned error handling by managing ClientSideException, ServerSideException or CommunicationException separately.

// Get API
SynchronousLndAPI synchronousLndAPI = getSynchronousLndAPI();

try{
    // Perform a call
    synchronousLndAPI.channelBalance();
}catch(ValidationException ve){
    // Thrown if request or response contained invalid data
}catch(StatusException se){
    // Thrown if GRPC related exception happened.
}

// Example of more fine grained exception handling.
try{
    synchronousLndAPI.channelBalance();
}catch(ValidationException ve){
    // Thrown if request or response contained invalid data
}catch(ClientSideException cse){
    // Thrown if there is some problem on the client side such as invalid request data.
}catch(ServerSideException sse){
    // Thrown if there is some problem on the server side that might persist for some time.
}catch(CommunicationException ce){
    // Thrown if communication problems occurred such as  timeout or dropped package and request can be retried.
}

AsynchronousLndAPI asynchronousLndAPI = getAsynchronousLndAPI();

asynchronousLndAPI.channelBalance(new StreamObserver<ChannelBalanceResponse>() {
    @Override
    public void onNext(ChannelBalanceResponse value) {
        // Handle ok resonses
    }

    @Override
    public void onError(Throwable t) {
        // Here is exceptions sent of same type as thrown by synchronous API.
    }

    @Override
    public void onCompleted() {
        // Call completed
    }
});
Status Code to High Level Status Exception Mappings

Below is a table detailing which high level excpetion is thrown for a given status code.

Table 2. Status Code to High Level Status Exception Mappings
Status Code Exception

CANCELLED

ClientSideException

UNKNOWN

ServerSideException

INVALID_ARGUMENT

ClientSideException

DEADLINE_EXCEEDED

CommunicationException

NOT_FOUND

ClientSideException

ALREADY_EXISTS

ClientSideException

PERMISSION_DENIED

ClientSideException

RESOURCE_EXHAUSTED

ServerSideException

FAILED_PRECONDITION

ServerSideException

ABORTED

ServerSideException

OUT_OF_RANGE

ClientSideException

UNIMPLEMENTED

ServerSideException

INTERNAL

ServerSideException

UNAVAILABLE

CommunicationException

DATA_LOSS

ServerSideException

UNAUTHENTICATED

ClientSideException

Low Level API

The low level API throws either the io.grpc.StatusException and io.grpc.StatusRuntimeException when problems occur containing a Status value. See GRPC Java documentation for more details.

Logging

The library uses the standard java.logging API for logging. Which is the same library as the underlying GRPC Java uses.

It has one Logger defined "org.lightningj.lnd.wrapper.API" and it is possible to setting it to LogLevel.FINE to have incoming and outgoing messages logged in pretty printed JSON format to help out when debugging.

Using the Low Level API Directly

If performance is most important and there is no need for JSON/XML convertion in your project you can use the auto-generated GRPC API directly.

It is generated from the LND rpc.proto specification and contains all supported messages and calls.

Example for using the low level API :

File trustedServerCertificate = new File(System.getProperty("user.home") + "/Library/Application Support/Lnd/tls.cert");
// Method to create SSL Context, trusting a specified LND node TLS certificate.
// It is possible to customize the SSL setting by supplying a javax.net.ssl.SSLContext as well
SslContext sslContext = GrpcSslContexts.configure(SslContextBuilder.forClient(), SslProvider.OPENSSL)
        .trustManager(trustedServerCertificate)
        .build();

// Then create a managed communication channed
ManagedChannel channel = NettyChannelBuilder.forAddress("localhost", 10001)
        .sslContext(sslContext)
        .build();

// Then create the low level API by calling.
LightningGrpc.LightningBlockingStub stub = LightningGrpc.newBlockingStub(channel);
// To create asynchronous API us LightningGrpc.newStub(channel)

// Create a request object using messages in "org.lightningj.lnd.proto.LightningApi"
LightningApi.WalletBalanceRequest.Builder walletBalanceRequest = LightningApi.WalletBalanceRequest.newBuilder();
walletBalanceRequest.setWitnessOnly(true);
try{
    LightningApi.WalletBalanceResponse response = stub.walletBalance(walletBalanceRequest.build());
    System.out.println("Wallet Balance: " + response.getTotalBalance());
}catch(StatusRuntimeException sre){
    // Handle exceptions a with status code in sre.getStatus()
}

More info about using GRPC Java API can be found at their Github or a their tutorial site.

JavaDoc API Documentation

The LightningJ JavaDoc API Reference can be found here.

Dependencies

A dependency report on dependent JAR files can be found here.

To view the requirements for run-time see the runtime section.

The JSON Libraries is built upon JSR 374 and probably can the glassfish dependency be replaced with whatever JSR 374 compliant implementation used by your container.

Using Intellij

If using LigtningJ source code with Intellij, there can be a problem with the generated low level API class files being too large.

To fix this must the accepted file size be enhanced. This can be done by:

  • In Menu: Help → Edit Custom Properties

  • In idea.properties add:

    idea.max.intellisense.filesize=3000
  • Restart IntelliJ

Test Reports

A report of performed unit tests of the API can be found here.

For LightningJ Developers

LightningJ is a Java project built using Gradle. Unit tests is written using Groovy and Spock Framework.

To build the project use:

./gradlew build

The build jar file is located in build/libs.

To generate documentation use:

./gradlew build doc

This will generate documentation in build/docs/html5.

To clean the project use:

./gradlew build doc

How to update rpc.proto file

  • Download the file from the LND repository:

    lnd/lnrpc/rpc.proto
  • Update file into src/main/proto/lightning.api.proto

  • In the header of the file below 'package lnrpc' add:

    option java_package = "org.lightningj.proto";
  • Then run

    ./gradlew clean build

GPG Sign Releases using SmartCard

To GPG Sign generated archives before publishing them to central repository using GPG Smartcard make sure to configure the following in ~/.gradle/gradle.properties

signing.gnupg.executable=gpg2
signing.gnupg.useLegacyGpg=false
signing.gnupg.keyName=<your key id>