Creating an API
Our first coding task is going to be creating a REST API of our own to provide graph information to our application. We'll start by getting our server connected to Alice's LND node.
Connecting to Alice's node
We've chosen to connect to LND for this application but we could just as easily use c-lightning or Eclair.
LND also has a Builder's Guide that you can explore to learn about common tasks.
LND has two ways we can interact with it from code: a REST API and a gRPC API. gRPC is a high performance RPC framework. With gRPC, the wire protocol is defined in a protocol definition file. This file is used by a code generators to construct a client in the programming language of your choice. gRPC is a fantastic mechanism for efficient network communication, but it comes with a bit of setup cost. The REST API requires less effort to get started but is less efficient over the wire. For applications with a large amount of interactivity, you would want to use gRPC connectivity. For this application we'll be using the REST API because it is highly relatable for web developers.
LND API Client
Inside our server
sub-project is the start of code to connect to LND's REST API. We'll add to this for our application.
Why are we not leveraging an existing library from NPM? The first reason is that it is a nice exercise to help demonstrate how we can build connectivity. Lightning Network is still a nascent technology and developers need to be comfortable building tools to help them interact with Bitcoin and Lightning Network nodes. The second and arguably more important reason is that as developers in the Bitcoin ecosystem, we need to be extremely wary of outside packages that we pull into our projects, especially if they are cryptocurrency related. Outside dependencies pose a security risk that could compromise our application. As such, my general rule is that runtime dependencies should generally be built unless it is burdensome to do so and maintain.
With that said, point your IDE at the server/src/domain/lnd/LndRestTypes.ts
file. This file contains a subset of TypeScript type definitions from the REST API documentation. We are only building a subset of the API that we'll need for understanding the graph.
Exercise : Defining the Graph
Type
In LndRestTypes
you'll see our first exercise. It requires us to define the resulting object obtained by calling LND's /v1/graph
API. You will need to add two properties to the Graph
interface, one called nodes
that is of type LightningNode[]
and one called edges
that of type ChannelEdge[]
. The LightningNode
and ChannelEdge
types are already defined for you.
// server/src/domain/lnd/LndRestTypes
export interface Graph {
// Exercise: define the `nodes` and `edges` properties in this interface.
// These arrays of LightningNode and ChannelEdge objects.
}
Exercise: Making the Call
Now that we've defined the results from a call to /v1/graph
, we need to point our IDE at server/src/domain/lnd/LndRestClient.ts
so we can write the code that makes this API call.
LndRestClient
implements a basic LND REST client. We can add methods to it that are needed by our application. It also takes care of the heavy lifting for establishing a connection to LND. You'll notice that the constructor takes three parameters: host
, macaroon
, and cert
. The macaroon
is similar to a security token. The macaroon that you provide will dictate the security role you use when calling the API. The cert
is a TLS certificate that enables a secure and authenticated connection to LND.
// server/src/domain/lnd/LndRestClient
export class LndRestClient {
constructor(
readonly host: string,
readonly macaroon: Buffer,
readonly cert: Buffer
) {}
}
This class also has a get
method that is a helper for making HTTP GET requests to LND. This helper method applies the macaroon and ensures the connection is made using the TLS certificate.
Your next exercise is to implement the getGraph
method in server/src/domain/lnd/LndRestClient.ts
. Use the get
helper method to call the /v1/graph
API and return the results. Hint: You can access get
with this.get
.
// server/src/domain/lnd/LndRestClient
public async getGraph(): Promise<Lnd.Graph> {
// Exercise: use the `get` method below to call `/v1/graph` API
// and return the results
}
After this is complete, we should have a functional API client. In order to test this we will need to provide the macaroon and certificate.
Exercise: Configuring .env
to Connect to LND
In this application we use the dotenv
package to simplify environment variables. We can populate a .env
file with key value pairs and the application will treat these as environment variables.
Environment variables can be read in Node.js from the process.env
object. So if we have an environment variable PORT
:
$ export PORT=8000
$ node app.js
This environment variable can be read with:
const port = process.env.PORT;
Our next exercise is adding some values to .env
inside the server
sub-project. We'll add three new environment variables:
LND_HOST
is the host where our LND node residesLND_READONLY_MACAROON_PATH
is the file path to the readonly MacaroonLND_CERT_PATH
is the certificate we use to securely connect with LND
Fortunately, Polar provides us with a nice interface with all of this information. Polar also conveniently puts files in our local file system to make our lives as developers a bit easier.
In Polar, to access Alice's node by click on Alice and then click on the Connect
tab. You will be shown the information on how to connect to the GRPC and REST interfaces. Additionally you will be given paths to the network certificates and macaroon files that we will need in .env
.
Go ahead and add the three environment variables defined above to .env
.
# Express configuration
PORT=8001
# LND configuration
# Exercise: Provide values for Alice's node
LND_HOST=
LND_READONLY_MACAROON_PATH=
LND_CERT_PATH=
Exercise: Reading the Options
Now that our environment variables are in our configuration file, we need to get them into the application. The server project uses server/src/Options
to read and store application options.
The class contains a factory method fromEnv
that allows us to construct our options from environment variables. We're going to modify the Options
class to read our newly defined environment variables.
This method is partially implemented, but your next exercise is to finish the method by reading the cert file into a Buffer. You can use the fs.readFile
method to read the path provided in the environment LND_CERT_PATH
environment variable. Note: Don't forget to use await
since fs.readFile
is an asynchronous operation.
// server/src/Options
public static async fromEnv(): Promise<Options> {
const port: number = Number(process.env.PORT),
const host: string = process.env.LND_HOST,
const macaroon: Buffer = await fs.readFile(process.env.LND_READONLY_MACAROON_PATH),
// Exercise: Using fs.readFile read the file in the LND_CERT_PATH
// environment variable
const cert: Buffer = undefined;
return new Options(port, host, macaroon, cert);
}
Exercise: Create the LND client
The last step before we can see if our application can connect to LND is that we need to create the LND client! We will do this in the entrypoint of our server code server/src/Server
.
In this exercise, construct an instance of the LndRestClient
type and supply it with the options found in the options
variable. Note: You can create a new instance of a type with the new
keyword followed by the type and a parameters, eg: new SomeClass(param1, param2)
// server/src/Server
async function run() {
// construct the options
const options = await Options.fromEnv();
// Exercise: using the Options defined above, construct an instance
// of the LndRestClient using the options.
const lnd: LndRestClient = undefined;
// construct an IGraphService for use by the application
const graphAdapter: IGraphService = new LndGraphService(lnd);
At this point, our server code is ready. We'll take a look at a few other things before we give it a test.
Looking at LndGraphService
The LndRestClient
instance that we just created will be used by LndGraphService
. This class follows the adapter design pattern: which is a way to make code that operates in one way, adapt to another use. The LndGraphService
is the place where we make the LndRestClient
do things that our application needs.
export class LndGraphService extends EventEmitter implements IGraphService {
constructor(readonly lnd: LndRestClient) {
super();
}
/**
* Loads a graph from LND and returns the type. If we were mapping
* the returned value into a generic Graph type, this would be the
* place to do it.
* @returns
*/
public async getGraph(): Promise<Lnd.Graph> {
return await this.lnd.getGraph();
}
For the purposes of fetching the graph, we simply call getGraph
on the LndRestClient
and return the results. But if we modified our application to use a generic graph instead of the one returned by LND, we could do that translation between the Lnd.Graph
type and our application's graph here.
At this point your server should capable of connecting to LND!
Looking at the Graph API
Since we're building a REST web service to power our front end application, we need to define an endpoint in our Express application.
Take a look at server/src/Server
. We're doing a lot of things in this file for simplicity sake. About half-way down you'll see a line:
// server/src/Server
app.use(graphApi(graphAdapter));
This code attaches a router to the Express application.
The router is defined in server/src/api/GraphApi
. This file returns a function that accepts our IGraphService
that we were just taking a look at. You can then see that we use the IGraphService
inside an Express request handler and then return the graph as JSON.
// server/src/api/GraphApi
export function graphApi(graphService: IGraphService): express.Router {
// Construct a router object
const router = express();
// Adds a handler for returning the graph. By default express does not
// understand async code, but we can easily adapt Express by calling
// a promise based handler and if it fails catching the error and
// supplying it with `next` to allow Express to handle the error.
router.get("/api/graph", (req, res, next) => getGraph(req, res).catch(next));
/**
* Handler that obtains the graph and returns it via JSON
*/
async function getGraph(req: express.Request, res: express.Response) {
const graph = await graphService.getGraph();
res.json(graph);
}
return router;
}
Dev Note: Express does not natively understand async
code but we can easily retrofit it. To do this we define the handler with a lambda function that has arguments for the Request
, Response
, and next
arguments (has the type (req, res, next) => void
). Inside that lambda, we then call our async code and attach the catch(next)
to that function call. This way if our async
function has an error, it will get passed to Express' error handler!
We can now run npm run watch
at the root of our application and our server should start up and connect to LND without issue.
If you're getting errors, check your work by making sure Polar is running, the environment variables are correct, and you've correctly wired the code together.
You can now access http://localhost:8001/api/graph in your browser and you'll see information about the network as understood by Alice!