Requisitos:
- NodeJs >= 8
- LND >= 9
NodeJs puede ser descargado en su sitio oficial https://nodejs.org
En lugar de descargar y configurar un nodo LND, vamos a utilizar la herramienta polar, la cual realizará esta tarea por nosotros.
Stack Para construir nuestra Lightning app, estaremos utilizando las siguientes tecnologías:
-
Express para nuestro webserver
-
Pug templates + bootstrap para nuestro frontend
-
Sistema operativo
Se recomienda utilizar Linux, si estas en windows 10 puedes tener una consola linux siguiendo estos pocos pasos:
https://youtu.be/Cvrqmq9A3tA?t=112
- Preparando la base
Utilice la herramienta de generador de aplicaciones, express, para crear rápidamente un esqueleto de aplicación.
$ sudo npm install express-generator -g
Con la siguiente instrucción creamo una aplicación Express denominada lnapp. La aplicación será creada en un directorio llamado lnapp en el directorio de trabajo actual y el motor de vistas será asignado a Pug.
$ express --view=pug lnapp
Luego entramos al directorio e instalamos los paquetes necesarios para que el web server corra.
$ cd myapp
$ npm install
Ahora simplemente corremos el server
$ npm start
A continuación, vaya esta dirección http://localhost:3000/ en el navegador para acceder a la aplicación.
La aplicación generada tiene la siguiente estructura de directorios:
.
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.pug
├── index.pug
└── layout.pug
Polar
Descargamos Polar, lo instalamos y creamos una red con 2 nodos LND (Alice y Bob) y 1 bitcoind, una vez veamos en la app el gráfico donde aparecen nuestros nodos hacemos clic en el botón "Comienzo" y espera unos segundos hasta que el indicador de cada nodo cambie de color a verde.
Para poder enviar pagos en Lightning es necesario que los nodos estén interconectados por medio de canales, crear canales con Polar es muy sencillo, solo necesitamos hacer clic con el mouse en una de las orejas del nodo Alice y arrastrarlo hasta una de las orejas del nodo Bob, inmediatamente te debe aparecer una ventana modal titulada "Abrir nuevo canal", dejamos los valores por defecto y presionamos el botón de abrir canal, ahora repetimos la acción pero esta vez desde Bob hacia alice, de esta manera los dos nodos pueden enviar y recibir dinero.
Nodemon
Para no tener que reiniciar nodejs cada vez que realicemos un cambio en el código instalaremos nodemon
$ npm install nodemon -D
Debemos crear una entrada en la sección "scripts" del archivo package.json, agregue esta línea "dev": "nodemon ./bin/www"
, la sección scripts debería quedar así:
"scripts": {
"start": "node ./bin/www",
"dev": "nodemon ./bin/www"
},
Vamos a la cónsola donde está corriendo npm start, hacemos ctrl + c y volvemos a iniciar nodejs pero esta vez utilizando nodemon
$ npm run dev
Conectándonos a LND
Para poder conectarnos a un nodo Lightning desde nodejs, una dependencia llamada ln-service, también instalaremos dotenv para administrar las variables de entorno.
$ npm install ln-service dotenv
En nuestro directorio lnapp creamos un archivo llamado .env, debe contener estas variables
LND_GRPC_HOST=''
LND_CERT_BASE64=''
LND_MACAROON_BASE64=''
Volvamos a Polar, seleccionamos a Bob, el nodo al que nos queremos conectar, vamos a la pestaña "Conectar", copiamos el contenido de "Host GRPC" y lo colocamos en la variable LND_GRPC_HOST, en la parte de abajo de la pestaña conectar seleccionamos "base64" y copiamos el contenido de "TLS Cert" y lo colocamos en la variable LND_CERT_BASE64 y finalizamos haciendo lo mismo con el "admin macaroon" en LND_MACAROON_BASE64.
Ahora agregamos esta línea al archivo app.js ubicado en la raiz del directorio de trabajo, debemos copiarlo en la primera línea del archivo.
require('dotenv').config();
Para verificar que nodejs puede conectarse con nuestro nodo abrimos el archivo routes/index.js
, este archivo de rutas fue creado por el generador express, primero requerimos la libreria ln-service
, así que agregamos en la primera línea
const lnservice = require('ln-service');
Añadimos una nueva ruta que nos dará la información de nuestro nodo.
router.get('/info', async function(req, res, next) {
try {
// nos autenticamos con lnd
const { lnd } = lnservice.authenticatedLndGrpc({
cert: process.env.LND_CERT_BASE64,
macaroon: process.env.LND_MACAROON_BASE64,
socket: process.env.LND_GRPC_HOST,
});
// obtenemos información del nodo
const info = await lnservice.getWalletInfo({ lnd });
// mostramos la info en formato json
res.send(`
<h1>Node info</h1>
<pre>${JSON.stringify(info, null, 2)}</pre>
`);
next();
} catch (e) {
console.log(e);
}
});
Ahora vamos esta dirección http://localhost:3000/info
Si ves un json con la información del nodo LND, felicidades!!! tu app ya puede interactual con Lightning Network.
Para simular una base de datos necesitamos crear un modelo falso, esto nos permitirá utilizar la app sin necesidad de instalar y configurar una base de datos.
Primero instalamos el paquete uuid
$ npm install uuid
Crea el directorio models
y dentro el archivo Post.js
con el siguiente contenido:
const { v4: uuidv4 } = require('uuid');
class Post {
constructor() {
this.posts = [];
}
add({ time, name, content, paid, hash, preimage, request }) {
const post = {
id: uuidv4(),
time: time || new Date(),
name,
content,
paid: paid || false,
hash: hash || null,
preimage: preimage || null,
request: request || null,
};
this.posts.push(post);
return post;
}
find(id) {
return this.posts.find(p => p.id === id);
}
findByHash(hash) {
return this.posts.find(p => p.hash === hash);
}
findAll() {
return this.posts;
}
findAllPaid() {
return this.posts
.filter(p => !!p.paid)
.sort((a, b) => b.time - a.time);
}
paid(hash) {
let updatedPost;
this.posts = this.posts.map(p => {
if (p.hash === hash) {
updatedPost = { ...p, paid: true };
return updatedPost;
}
return p;
});
if (updatedPost) {
return true;
}
return false;
}
};
const posts = new Post();
module.exports = posts;
Necesitamos boostrap para que nuestro html se vea mejor, para ello modificamos el archivo layout.pug
ubicado en el directorio views
para que quede así:
doctype html
html
head
title= title
link(rel='stylesheet', href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css')
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
block scripts
script(src="https://code.jquery.com/jquery-3.4.1.min.js")
script(src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js")
script(src="/javascripts/main.js")
Para crear un post necesitamos un formulario, dentro del directiorio views
creamos un archivo llamado form.pug
que contenga:
.collapse#post-form
form
.h2 Escriba un artículo
.form-group
label(for="name") Npmbre
input(id="name").form-control
.form-group
label(for="content") Contenido
textarea(id="content").form-control
button.btn.btn-primary#send-btn(type="button") Enviar
La manera más directa que tenemos para interactuar con el usuario es utilizando javascript en el web broswer, para esto, en el directorio javascript
creamos un archivo main.js
que ya estamos llamando desde el layout.pug
, en el cual inicializamos el proyecto, el contenido inicial de main.js
es el siguiente:
const App = {
endpoint: 'http://localhost:3000/api',
interval: null,
};
App.init = () => {
$('#post-form').collapse('show');
$('#send-btn').click(App.sendBtn);
}
App.sendBtn = () => {
console.log('!hola');
};
$(() => App.init());
Presionamos el botón Enviar y si todo está bien debe mostrarnos en la cónsola un mensaje !hola, con esto ya podemos modificar el método App.sendBtn()
para que envíe la información a nuestra API.
App.sendBtn = async () => {
const name = $('#name').val();
const content = $('#content').val();
const response = await App.makeRequest({
api: "post",
post: { name, content },
});
if (!response) console.error('Error getting data!');
if (response.success) {
// imprimimos en la consola la data que viene de la API
console.log(response.data);
}
};
Tambien creamos el método App.makeRequest()
y también lo agregamos a main.js
App.makeRequest = ({api, post}) => {
const type = !post ? 'GET' : 'POST';
const data = !!post ? JSON.stringify(post) : null;
return $.ajax(`${App.endpoint}/${api}`, {
type,
data,
contentType: 'application/json',
dataType: 'json',
});
};
Para esto debemos crear un nuevo archivo en routes/api.js
con y dentro escribimos el método que responde a la solicitud que hacemos dentro de App.sendBtn()
.
const express = require('express');
const lnservice = require('ln-service');
const router = express.Router();
const post = require('../models/Post');
router.post('/post', (req, res) => {
const { name, content } = req.body;
return res.json({
success: true,
data: { name, content },
});
});
module.exports = router;
Este archivo debe ser incluido en app.js
, para esto agregamos estas líneas:
const apiRouter = require('./routes/api');
app.use('/api', apiRouter);
Volvamos a presionar el botón enviar y nos debe responder con la misma data que escribimos en el formulario.
El método que se ejecuta cuando un usuario crea un post, debe generar una invoice, luego crear un registro en la base de datos vinculándolo a la invoice y retornarle al usuario la invoice para que pueda pagarla.
router.post('/post', async (req, res) => {
// nos autenticamos con lnd
const { lnd } = lnservice.authenticatedLndGrpc({
cert: process.env.LND_CERT_BASE64,
macaroon: process.env.LND_MACAROON_BASE64,
socket: process.env.LND_GRPC_HOST,
});
const { name, content } = req.body;
try {
const invoice = await lnservice.createInvoice({
lnd,
tokens: 1000,
description: name,
});
if (!!invoice) {
const p = post.add({
name,
content,
hash: invoice.id,
request: invoice.request,
preimage: invoice.secret,
});
return res.json({
success: true,
data: p,
});
}
} catch (e) {
console.log(e);
}
});
Si luego de presionar enviar recibimos un objeto post como respuesta, de este tipo, todo ha salido correctamente, ahora debemos mostrar el texto para que el usuario pueda pagarlo.
{
"success":true,
"data":{
"id":"0fb1544a-d7f9-487d-961a-d0004ecc515c",
"time":"2020-09-02T21:29:53.747Z",
"name":"epale",
"content":"contenido bla bla",
"paid":false,
"hash":"0e3c7a1151d8f8f202bc7264db83e554c9009f9bd32c0fcb0412772b310b520e",
"preimage":null,
"request":"lnbcrt10u1p04qrk3pp5pc785y23mru0yq4uwfjdhql92nysp8um6vkqljcyzfmjkvgt2g8qdqgv4cxzmr9cqzpgxq92fjuqsp534n0k3up43xlrnwmh97kk9mtl89n7yvkrznhrmqd47yll8eux35s9qy9qsqc2nlv8nd5k9tam8m9tzrqr05n3dzllsqxzsxs62j2zl7k49up834eazq4lhpfkjysmgevu5memgj7g7cq0fujsqvf49et88sl7v8zlcq4mgxx9"
}
}
En el directorio views
debemos crear un archivo llamado invoice.pug
con el siguiente contenido:
.collapse#invoice-form
form
.h2 Pague esta factura
.form-group
textarea(
id="invoice",
readonly,
rows="5"
).form-control
Y la incluimos en index.pug
extends layout
block content
h1 Lightning App
include form.pug
include invoice.pug
Ahora modificamos App.sendBtn() para que muestre la data obtenida:
App.sendBtn = async () => {
const name = $('#name').val();
const content = $('#content').val();
const response = await App.makeRequest({
api: "post",
post: { name, content },
});
if (!response) console.error('Error getting data!');
if (response.success) {
$('#post-form').collapse('hide');
$('#invoice-form').collapse('show');
$('#invoice').val(response.data.request);
}
};
Necesitamos saber cuándo recibimos el pago, para esto vamos a utilizar el método subscribeToInvoices()
de lnservice
, este método nos permite ejecutar código cuando el estado de una invoice ha sido actualizado, para utilizarlo agregamos estas líneas en app.js
.
// hacemos el require de lnservice y de nuestra tabla post
const lnservice = require('ln-service');
const post = require('./models/Post');
// nos autenticamos con lnd
const { lnd } = lnservice.authenticatedLndGrpc({
cert: process.env.LND_CERT_BASE64,
macaroon: process.env.LND_MACAROON_BASE64,
socket: process.env.LND_GRPC_HOST,
});
// cada vez que ocurra un cambio en una invoice se chequea si la invoice
// ha sido pagada
const subscribeInvoices = async () => {
try {
const sub = lnservice.subscribeToInvoices({lnd});
sub.on('invoice_updated', async invoice => {
if (!invoice.is_confirmed) return;
// cambia la invoice a 'pagada'
const paid = post.paid(invoice.id);
if (paid) console.log('Invoice paid!');
});
} catch (e) {
console.log(e);
return e;
}
};
// iniciamos la subscripcion a invoices
subscribeInvoices();
Creamos en nuestra API un método HTTP GET para que el usuario pueda consultar si un hash ha sido pagado.
router.get('/post/:hash', (req, res) => {
const { hash } = req.params;
const data = post.findByHash(hash);
if (!!data) {
return res.json({
success: true,
data,
});
} else {
return res.json({
success: false,
data: null,
});
}
});
Ahora desde main.js
creamos una función llamada App.waitPayment()
que consulta a la api si el pago ha sido realizado.
App.waitPayment = async (hash) => {
const response = await App.makeRequest({
api: `post/${hash}`,
});
if (response.success && response.data.paid) {
console.log("pago realizado");
}
};
Ahora nos encontramos un problema, la función App.waitPayment()
se ejecuta solo una vez, el usuario puede haber pagado y no le hemos podido indicar que su pago ha sido recibido, para esto utilizamos una función de javascript llamada setInterval()
que nos permite ejecutar una función indefinidamente cada intérvalo de tiempo que le hayamos indicado.
Modifiquemos las funciones App.waitPayment()
y App.sendBtn()
incluyendo setInterval()
y clearInterva()
App.waitPayment = async (hash) => {
const response = await App.makeRequest({
api: `post/${hash}`,
});
if (response.success && response.data.paid) {
clearInterval(App.interval);
App.interval = null;
$('#invoice-form').collapse('hide');
$('#preimage').text(response.data.preimage);
$('#success').collapse('show');
}
};
App.sendBtn = async () => {
const name = $('#name').val();
const content = $('#content').val();
const response = await App.makeRequest({
api: "post",
post: { name, content },
});
if (!response) console.error('Error getting data!');
if (response.success) {
$('#post-form').collapse('hide');
$('#invoice-form').collapse('show');
$('#invoice').val(response.data.request);
App.interval = setInterval(App.waitPayment, 1000, response.data.hash);
}
};
Y creamos una vista para indicar que el pago ha sido recibido exitosamente, creamos el archivo success.pug
en views
con el siguiente contenido:
.collapse#success
.h2 Pago exitoso
.div Prueba de pago:
#preimage
Lo incluimos en index.pug
.
extends layout
block content
h1 Lightning App
include form.pug
include invoice.pug
include success.pug
Si luego de pagar la invoice puedes ver el mensaje de Pago exitoso y la prueba de pago felicidades!!! lo lograste, has terminado tu primera LApp.