Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First basic draft version of a java client library. need feedback #2223

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions contrib/JLightning/README.md
@@ -0,0 +1,14 @@
Draft version of the JLightning rpc interface for c-lightning.

Using this client library is is as simple as the following code:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if someone is familiar with java, would be nice to have the actual command to build and launch: e.g.

$ javac <details-to-build>
$ java <details-to-run>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On that note, have you though of using Gradle or Maven to handle building and dependency management? Preferably Gradle

```
public static void main(String[] args) {
JLightningRpc rpc_interface = new JLightningRpc("/tmp/spark-env/ln1/lightning-rpc");
String res = rpc_interface.listInvoices(null);
System.out.println(res);
res = rpc_interface.listFunds();
System.out.println(res);
}
```

This client library is provided and maintained by Rene Pickhardt
36 changes: 36 additions & 0 deletions contrib/JLightning/src/de/renepickhardt/ln/JLightning.java
@@ -0,0 +1,36 @@
package de.renepickhardt.ln;

/**
* JLightning a small test / example class to demonstrate how to use JLightningRpc

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this into a README.md and add the necessary code snippets that you provide here in the main method.

*
* This file is basically a port of the pylightning python client library
* that comes with c-lightning.
*
* The Author of this Java Client library is Rene Pickhardt.
* He also holds the copyright of this file. The library is licensed with
* a BSD-style license. Have a look at the LICENSE file.
*
* If you like this library consider a donation via bitcoin or the lightning
* network at http://ln.rene-pickhardt.de
*
* @author Rene Pickhardt

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - extra line break.

*/

public class JLightning {

public static void main(String[] args) {
// TODO Auto-generated method stub

JLightningRpc rpc_interface = new JLightningRpc("/tmp/spark-env/ln1/lightning-rpc");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would inject this "/tmp/spark-env/ln1/lightning-rpc" via command line. Will be available in args.

String res = rpc_interface.listInvoices(null);
System.out.println(res);
res = rpc_interface.listFunds();
System.out.println(res);
res = rpc_interface.listInvoices(null);
System.out.println(res);

}


}
147 changes: 147 additions & 0 deletions contrib/JLightning/src/de/renepickhardt/ln/JLightningRpc.java
@@ -0,0 +1,147 @@
package de.renepickhardt.ln;
/**
* JLightningRpc extends the UnixDomainSocketRpc and exposes the specific
* API that is provided by c-lightning. It is a java client library for the
* c-lightning node. It connects to c-lightning via a Unix Domain Socket over
* JsonRPC v2.0
*
* This file is basically a port of the pylightning python client library
* that comes with c-lightning.
*
* The Author of this Java Client library is Rene Pickhardt.
* He also holds the copyright of this file. The library is licensed with
* a BSD-style license. Have a look at the LICENSE file.
*
* If you like this library consider a donation via bitcoin or the lightning
* network at http://ln.rene-pickhardt.de
*
* @author Rene Pickhardt
*/

import java.util.HashMap;



public class JLightningRpc extends UnixDomainSocketRpc {

public JLightningRpc(String socket_path) {
super(socket_path);

}

/**
* Delete unpaid invoice {label} with {status}
* @param label of the invoice
* @param status status of the invoice
* @return
*/
public String delInvoice(String label, Status status) {
HashMap<String, String> payload = new HashMap<String,String> ();
payload.put("label", label);
payload.put("status", status.toString());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To handle NullPointerException here on toString(), you can use String.valueOf(status). In case of null, it will return the String "null".

return this.call("delinvoice", payload);
}

public String getInfo() {
return this.call("getinfo", null);
}

/**
* Show route to {id} for {msatoshi}, using {riskfactor} and optional
* {cltv} (default 9). If specified search from {fromid} otherwise use
* this node as source. Randomize the route with up to {fuzzpercent}
* (0.0 -> 100.0, default 5.0) using {seed} as an arbitrary-size string
* seed.
*
* @param peer_id
* @param msatoshi
* @param riskfactor
* @param cltv
* @param from_id
* @param fuzzpercent
* @param seed
* @return
*/
public String getRoute(String peer_id, int msatoshi, float riskfactor, int cltv, String from_id, float fuzzpercent, String seed) {
HashMap<String, String> payload = new HashMap<String,String> ();
payload.put("id", peer_id);
payload.put("msatoshi", Integer.toString(msatoshi));
payload.put("riskfactor", Float.toString(riskfactor));
payload.put("cltv", Integer.toString(cltv));
payload.put("fromid", from_id);
payload.put("fuzzpercent", Float.toString(fuzzpercent));
payload.put("seed", seed);
return this.call("getroute", payload);
}

/**
* Create an invoice for {msatoshi} with {label} and {description} with
* optional {expiry} seconds (default 1 hour)
*
* @param msatoshi
* @param label
* @param description
* @param expiry
* @param fallbacks
* @param preimage
* @return
*/
public String invoice (int msatoshi,String label, String description, int expiry,String fallbacks, String preimage) {
HashMap<String, String> payload = new HashMap<String,String> ();
payload.put("msatoshi", Integer.toString(msatoshi));
payload.put("label", label);
payload.put("description", description);
payload.put("expiry", Integer.toString(expiry));
payload.put("fallbacks", fallbacks);
payload.put("preimage", preimage);
return this.call("invoice", payload);
}

/**
* Show funds available for opening channels and open channels
* @return
*/
public String listFunds() {
return this.call("listfunds", null);
}

/**
* Show all known channels, accept optional {short_channel_id}
*/
public String listChannels(String short_channel_id) {
HashMap<String, String> payload = new HashMap<String,String> ();
payload.put("short_channel_id", short_channel_id);
return this.call("listchannels", payload);
}

/**
* Show invoice {label} (or all, if no {label))
* @param label for a specific invoice to look up
* @return
*/
public String listInvoices(String label) {
HashMap<String, String> payload = new HashMap<String,String> ();
payload.put("label", label);
return this.call("listinvoices", payload);
}

public String listNodes(String node_id) {
HashMap<String, String> payload = new HashMap<String,String> ();
payload.put("id", node_id);
return this.call("listnodes", payload);
}

/**
* Wait for the next invoice to be paid, after {lastpay_index}
* (if supplied)
* @param last_payindex
* @return
*/
public String waitAnyInvoice(int last_payindex) {
HashMap<String, String> payload = new HashMap<String,String> ();
payload.put("last_pay_index", Integer.toString(last_payindex));
return this.call("waitanyinvoice", payload);
}


}
29 changes: 29 additions & 0 deletions contrib/JLightning/src/de/renepickhardt/ln/Status.java
@@ -0,0 +1,29 @@
package de.renepickhardt.ln;
/**
* Named enum to encode the Status of invoices and payments
*
* This file is basically a port of the pylightning python client library
* that comes with c-lightning.
*
* The Author of this Java Client library is Rene Pickhardt.
* He also holds the copyright of this file. The library is licensed with
* a BSD-style license. Have a look at the LICENSE file.
*
* If you like this library consider a donation via bitcoin or the lightning
* network at http://ln.rene-pickhardt.de
*
* @author rpickhardt
*/
public enum Status {
PAID("paid"), UNPAID("unpaid"), EXPIRED("expired");

private final String statusDescription;

private Status(String value) {
statusDescription = value;
}

public String getStatusDescription() {
return statusDescription;
}
}
119 changes: 119 additions & 0 deletions contrib/JLightning/src/de/renepickhardt/ln/UnixDomainSocketRpc.java
@@ -0,0 +1,119 @@
package de.renepickhardt.ln;
/**
* UnixDomainSocketRpc the base class to handle communication between
* JLightning and the c-lightning node over the UnixDomainSocket
*
* This file is basically a port of the pylightning python client library
* that comes with c-lightning.
*
* The Author of this Java Client library is Rene Pickhardt.
* He also holds the copyright of this file. The library is licensed with
* a BSD-style license. Have a look at the LICENSE file.
*
* If you like this library consider a donation via bitcoin or the lightning
* network at http://ln.rene-pickhardt.de
*
* @author Rene Pickhardt
*/

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;


import org.json.JSONException;
import org.json.JSONObject;
import org.newsclub.net.unix.AFUNIXSocket;
import org.newsclub.net.unix.AFUNIXSocketAddress;

public class UnixDomainSocketRpc {
protected AFUNIXSocket sock;
protected InputStream is;
protected OutputStream os;
protected static int id = 1;

public UnixDomainSocketRpc(String socket_path) {
File socketFile = new File(socket_path);
try {
this.sock = AFUNIXSocket.newInstance();
this.sock.connect(new AFUNIXSocketAddress(socketFile));
this.is = sock.getInputStream();
this.os = sock.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}

public String call(String method, HashMap<String, String> payload) {
// if no payload is given make empty one
if(payload == null) {
payload = new HashMap<String, String>();
}

//remove null items from payload
Copy link

@jeffrade jeffrade Feb 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to add the keys to a new HashSet here. You can simply iterate through the payload.keySet() and and remove an entry if value is null.

Here's some sample code:

    Map<String, String> m = new HashMap<String, String>();
    m.put("1", "One");
    m.put("2", null);
    System.out.println(m);
    for(String k:m.keySet()) {
      m.remove(k, null);
    }
    System.out.println(m);

And output is:

{1=One, 2=null}
{1=One}

Set<String> keySet = new HashSet<String>();
for (String key: payload.keySet()) {
keySet.add(key);
}
for(String k:keySet) {
if(payload.get(k)==null){
payload.remove(k);
}
}

JSONObject json = new JSONObject();

try {
json.put("method", method);
json.put("params", new JSONObject(payload));
// FIXME: Use id field to dispatch requests
json.put("id", Integer.toString(this.id++));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.id isn't thread-safe since it is static. Use AtomicInteger instead and then call getAndIncrement().

} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

try {
this.os.write(json.toString().getBytes("UTF-8"));
this.os.flush();

// FIXME: Using a StringBuilder would be preferable but not so easy
String response = "";
int buffSize = 1024;
byte[] buf = new byte[buffSize];

while(true) {
int read = this.is.read(buf);
response = response + new String(buf, 0, read, "UTF-8");
if(read <=0 || response.contains("\n\n" )) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in my Haskell implementation I count open and closed curly brackets (ensuring they sum to 0) to determine when to stop reading.

This could fail in an unlikely event that both \n's appear at the end and start of two separate reads. This would result in the entire program blocking indefinitely on the next read call.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response is the string that has been appended with the buffer so I see no problem with split \n

On the other side the sum of open and closed brakets might fail as those could be part of the strings in the json data. So those need to be ignored

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah good point on the curly's. I see now that it gets appended before the check so yeah looks like it would be fine. I should probably do something similar... I do find this part a bit strange. It looks like lightning-cli does it by parsing tokens as they come in, and ends the reading when there is no more to parse.

break;
}
}
json = new JSONObject(response);
if (json.has("result")) {
json = json.getJSONObject("result");
return json.toString(2);
}
else if (json.has("error")) {
json = json.getJSONObject("error");
return json.toString(2);
}
else
return "Could not Parse Response from Lightning Node: " + response;

} catch (IOException e) {
e.printStackTrace();
// FIXME: make json rpc?
return "no Response from lightning node";
} catch (JSONException e) {
e.printStackTrace();
return "Could not parse response from Lightning Node";
}

}
}