Making A Lightning Web App: Part 2
Setting up a frontend and taking our very first payment
Hey, this is part 2 of a 5 part series on building a Lightning app. If you haven’t read part 1, you might want to check it out.
We left off from the previous tutorial with a basic web server that had a single page for serving up our node’s information. While it was a good start to understand how we communicate with our node, we need a proper frontend with interactivity and style, as well as expand the server to store and provide user data to really consider our project an ‘app’.
The idea for this tutorial application is to make a Twitter-style post feed, that users need to pay 1 satoshi per character to make posts. While not the most innovative or ground breaking application, hopefully this will be a good starting point to inspire you to come up with your own interesting use-cases that integrate micro-payments.
For this portion of the tutorial, we’re adding Webpack, React, and Bootstrap to the mix. Webpack will compile our code into files the browser knows how to deal with. React is the framework that will manage our frontend logic and rendering. Bootstrap will provide us with some nice components and styles so that we won’t have to write that all from scratch.
Let’s dive in!
Getting Started: Clone / Fetch the (new) Project
Like last time, we’ll start by cloning a further iteration of the project. This comes from the same repository, but it’s on a different branch. You can either clone the repository fresh, or if you already had it from last time, just fetch the latest data and checkout the branch:
# Run this if you don't already have the repository
git clone https://github.com/wbobeirne/lightning-app-tutorial.git
git checkout part-2
# Run this inside the repository directory if you already had it
git fetch origin
git checkout part-2
Like before, you’ll need to install the dependencies we need. We’ve added a few new ones, so you’ll need to re-run this in the project directory:
In addition to new dependencies, there are also some changes and additions to our old environment variables in the
# Server configuration
PORT=3001# Client configuration
API_PATH="http://localhost:3001/api"# LND Node configuration
We’ve got to make the following changes:
- Bump the
PORTvariable for Express up to
3001— Our client application will be served up by
webpack-dev-serverin development. This keeps our React client code and our REST API separate for now.
- Add the
API_PATHconfiguration — Since our REST API is served up on a different server than our frontend code, we need to tell the frontend where to go to make API requests.
- Update the
invoice.macaroon— Before we were using the
readonly.macaroon, but now we’ll need to generate invoices, which means we need more permissions. You’ll need to do the same
base64encoding we did in the previous post on
Once you’ve made those changes, you should be all ready to go.
We now have a new folder in our project called
client/. This will contain the code and configuration for the frontend. There's a lot of new code here, but the majority of it is standard React / Webpack boilerplate you see in a lot of projects. I won’t be covering most of that as there are other tutorials that will do a much better job than I ever could, so feel free to take a look elsewhere before continuing on.
There are also some changes to the Express server to make it function as a REST API for our frontend application to make requests against, and to maintain the posts that users submit.
The code we’ll be going over here will strictly be the stuff that interacts with our Lightning Node, and our server’s API. Those files will be:
server/index.ts— New API-style routes have been added that our client will hit to fetch and create user-submitted posts.
client/lib/api.ts— A simple API interface that handles requests to our API for those new routes.
client/components/PostForm.tsx— A form component for making a new post on our site, and paying the associated invoice.
client/components/Posts.tsx— A list component that displays all of the posts that have been made & paid for.
Now that we’ve got that overview, onto the code!
Returning to our simple server from the previous tutorial, we now have a few new routes that all interact with a
Posts model provided by the
Our first two routes are quite simple. The
GET /api/posts endpoint returns an array of posts that have been paid for, ordered by most recent. The
GET /api/posts/:id endpoint is one that gets a specific post, regardless of its payment status. These will be used for listing public posts, and checking on the status of your own post respectively.
The next, and more complicated route here is the
POST /api/posts endpoint, which will submit a new post, and generate an invoice associated with it that needs paying for:
This creates a new Post object, which is initially marked as unpaid. We then generate an invoice with our node, setting the
value (cost in satoshis) to be the number of characters in
content. We also embed the post ID number in the
memo. This will allow us to look up the post later once it’s been paid. We then return the unpaid post object, as well as the BOLT-11 payment request string to the user for them to pay.
It’s important to manage this data on the server, rather than the client. You don’t want to trust the client with logic around payments, as a savvy, nefarious user might send your server bad data, such as an extremely long post that they only pay 1 satoshi for, or find a way to bypass payment altogether.
Finally, down in our initialization logic, we have a bit of code that watches an invoice stream for paid invoices:
Here we get a
Readable stream object from our node API that allows us to tap into every invoice update from our node. This stream will cause the
.on('data') callback to trigger when invoices are created or paid, so we have a little logic in it to exclude:
- Newly created invoices that haven’t been paid yet
- Invoices that are potentially unrelated to our app and don’t have a memo that’s formatted with a post ID.
After we’ve ignored those, we can mark the post as paid with the ID we pulled out of the memo. The next time a user requests the list of paid posts via
GET /api/posts, this one will now show up.
Now, onto the new client code that will be running in the user’s browser:
This provides us nicely typed functions for our server API in a way that gives us methods that return promises for the endpoints we just went over. Don’t worry too much about the code in here, just know that we can call these methods now to get data from our server, or handle any errors that may happen. We’ll see how the API gets used in the next two files:
Here we’re fetching and displaying any and all posts that users have made (and paid for!) We make a request to our API to fetch the posts. If the request succeeds or fails, we store the posts or error message respectively in component state. The render method (not shown here) then renders markup based on what’s in state.
PostForm component has two important methods that we'll take a look at one at a time:
After filling out and submitting our form, the post is submitted to the backend. and if all goes well we get back the
res object with
post data, as well as a BOLT-11
paymentRequest string. We'll set these to our component's state so that we can render the payment request and a button to open with a wallet on our computer.
We also call the
checkIfPaid method when the submission succeeds:
This method will start checking if we’ve paid for the post that we’re currently showing an invoice for. It waits a second, checks, and if it hasn’t been paid, calls itself again recursively. This will run forever until we pay the invoice, which will cause it to refresh the page. Or if we leave the page, the post will never be made public, and the invoice will eventually expire.
With that all taken care of, all that’s left is to run the thing with
npm run dev and check out our functioning Lightning app!
There’s a bit of glue connecting all of this logic together that I didn’t go over explicitly, so feel free to poke around on your own to see how everything works. Or reach out to me with any specific questions.
And by all means, tinker with the thing. Make the cost increase exponential, or replace text posts with images, links, or tweets. Maybe add some of your own styling to really make it yours. Go nuts!
There’s still so much to improve here. Up next in part 3, we’ll replace the janky behavior of long-polling and refreshing the page with instantly-updating websockets. Get started on it out now!