Skip to content

Instantly share code, notes, and snippets.

@grunch
Created October 11, 2022 14:23
Show Gist options
  • Save grunch/7b7648072789eac0aa13e0db8b99126a to your computer and use it in GitHub Desktop.
Save grunch/7b7648072789eac0aa13e0db8b99126a to your computer and use it in GitHub Desktop.
Rust lightning workshop

Tutorial para desarrollar una Lightning App para recibir Propinas en Rust

Requisitos:

  • Rust >= 1.64.0
  • LND >= 0.14.2

Para instalar Rust debemos seguir las intrucciones en su sitio oficial

En lugar de descargar y configurar un nodo LND, vamos a utilizar la herramienta polar, la cual realizará esta tarea por nosotros, asegurate de instalar docker y docker-compose ya que es un requerimiento para utilizar polar.

Para construir nuestra Lightning app, estaremos utilizando Rocket.rs.

Sistema operativo

Se recomienda utilizar Linux, si estas en windows 10 puedes tener una consola linux siguiendo estos pocos pasos.

Preparando la base

Luego de instalar rust utilizaremos cargo para crear rápidamente un esqueleto de aplicación.

$ cargo new lntip

La aplicación generada tiene la siguiente estructura de directorios:

.
├── Cargo.lock
├── Cargo.toml
└── src
     └── main.rs

Con un editor de texto abrimos el archivo lntip/src/main.rs y vemos lo siguente:

fn main() {
    println!("Hello, world!");
}

Luego entramos al directorio simplemente corremos el server

$ cd lntip
$ cargo run

Instalamos el framework web rocket, el cual nos permitirá que nuestro programa sea un servidor web.

$ cargo add rocket@0.5.0-rc.2

Sustituimos el contenido de main.rs con:

#[macro_use]
extern crate rocket;

#[get("/hola/<name>/<age>")]
fn hello(name: &str, age: u8) -> String {
    format!("Hola, tienes {} años y te llamas {}!", age, name)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![hello])
}

Ejecutamos nuevamente el proyecto:

$ cargo run

A continuación, vamos esta dirección http://localhost:8000 en el navegador para acceder a la aplicación.

Polar

Descargamos Polar, lo instalamos y creamos una red con los valores por defecto, 3 nodos LND, (Alice, Bob y Carol), además de 1 nodo bitcoin core, presionamos el botón crear red, 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 de pago, 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, aumentamos la capacidad a 10_000_000 sats y presionamos el botón de abrir canal, repetimos la misma acción y creamos un canal de Bob a Carol, también de 10_000_000 sats.

El canal entre Alice y Bob, como fue creado por Alice, tiene los 10 millones de satoshis del lado de Alice, por cual, Alice puede realizar pagos pero no recibir pagos, lo mismo ocurre con el canal creado desde Bob a Carol, para permitir que todos los nodos puedan enviar y recibir creamos una factura por 5_000_000 sats con el nodo Carol y la pagamos con el nodo Alice.

cargo-watch

Para no tener que reiniciar el proyecto cada vez que realicemos un cambio en el código instalaremos cargo-watch

$ cargo install cargo-watch

Vamos a la cónsola donde está corriendo cargo run, presionamos ctrl + c y volvemos a iniciar el proyecto pero esta vez ejecutaremos cargo watch -x run.

Conectándonos a LND

Para poder conectarnos a un nodo Lightning desde rust, utilizaremos la librería tonic_openssl_lnd, también instalaremos dotenv para administrar las variables de entorno.

$ cargo add tonic_openssl_lnd dotenv

En nuestro directorio lntip creamos un archivo llamado .env, debe contener estas variables:

LND_GRPC_HOST=''
LND_GRPC_PORT=''
# path to tls.cert file
LND_CERT_FILE=''
# path to macaroon file
LND_MACAROON_FILE=''

Volvemos a Polar, seleccionamos a Alice, el nodo al que nos queremos conectar, vamos a la pestaña "Conectar", copiamos el contenido de Host GRPC, al copiarlo obtendremos algo como esto 127.0.0.1:10001, de aquí tomaremos la IP del Host (127.0.0.1) y la colocamos en la variable LND_GRPC_HOST, el puerto (10001) en LND_GRPC_PORT. En la parte de abajo de la pestaña conectar seleccionamos Rutas de archivo y copiamos el contenido de TLS Cert y lo colocamos en la variable LND_CERT_FILE y finalizamos haciendo lo mismo con el admin macaroon en LND_MACAROON_FILE.

Ahora agregamos esta línea al archivo main.rs ubicado en la raíz del directorio de trabajo, debemos copiarlo en la primera línea del archivo.

use dotenv::dotenv;
use std::env;

Para hacer nuestro código un poco más legible vamos a crear un archivo para manejar las rutas src/routes.rs, por ahora agreguemos una sola ruta index que nos devolverá un "Hola mundo!", el archivo quedará de esta manera:

use rocket::*;

#[get("/")]
pub fn index() -> &'static str {
    "Hola mundo!"
}

Chequeamos http://localhost:8000 en el navegador y deberíamos poder ver "Hola mundo!".

Conectar el proyecto al nodo lightning

Para conectar rust con nuestro nodo lightning creamos un nuevo archivo src/lightning.rs en el cual escribimos la función que realizará la conexión y nos retorna un cliente.

use dotenv::dotenv;
use std::env;
use tonic_openssl_lnd::{LndClientError, LndLightningClient};

pub async fn connect() -> Result<LndLightningClient, LndClientError> {
    dotenv().ok();
    let port: u32 = env::var("LND_GRPC_PORT")
        .expect("LND_GRPC_PORT must be set")
        .parse()
        .expect("port is not u32");
    let host = env::var("LND_GRPC_HOST").expect("LND_GRPC_HOST must be set");
    let cert = env::var("LND_CERT_FILE").expect("LND_CERT_FILE must be set");
    let macaroon = env::var("LND_MACAROON_FILE").expect("LND_MACAROON_FILE must be set");
    // Connecting to LND requires only host, port, cert file, and macaroon file
    let client = tonic_openssl_lnd::connect_lightning(host, port, cert, macaroon)
        .await
        .expect("Failed connecting to LND");

    Ok(client)
}

Creando una factura Lightning

Ahora vamos a crear una función que crea una factura lightning y la llamaremos create_invoice():

use tonic_openssl_lnd::lnrpc::{AddInvoiceResponse, Invoice}; // <-- al inicio

pub async fn create_invoice(
    client: &mut LndLightningClient,
    description: &str,
    amount: u32,
) -> Result<AddInvoiceResponse, LndClientError> {
    let invoice = Invoice {
        memo: description.to_string(),
        value: amount as i64,
        ..Default::default()
    };
    let invoice = client.add_invoice(invoice).await?.into_inner();

    Ok(invoice)
}

El archivo completo debe quedar así:

use dotenv::dotenv;
use std::env;
use tonic_openssl_lnd::lnrpc::{AddInvoiceResponse, Invoice};
use tonic_openssl_lnd::{LndClientError, LndLightningClient};

pub async fn connect() -> Result<LndLightningClient, LndClientError> {
    dotenv().ok();
    let port: u32 = env::var("LND_GRPC_PORT")
        .expect("LND_GRPC_PORT must be set")
        .parse()
        .expect("port is not u32");
    let host = env::var("LND_GRPC_HOST").expect("LND_GRPC_HOST must be set");
    let cert = env::var("LND_CERT_FILE").expect("LND_CERT_FILE must be set");
    let macaroon = env::var("LND_MACAROON_FILE").expect("LND_MACAROON_FILE must be set");
    // Connecting to LND requires only host, port, cert file, and macaroon file
    let client = tonic_openssl_lnd::connect_lightning(host, port, cert, macaroon)
        .await
        .expect("Failed connecting to LND");

    Ok(client)
}

pub async fn create_invoice(
    client: &mut LndLightningClient,
    description: &str,
    amount: u32,
) -> Result<AddInvoiceResponse, LndClientError> {
    let invoice = Invoice {
        memo: description.to_string(),
        value: amount as i64,
        ..Default::default()
    };
    let invoice = client.add_invoice(invoice).await?.into_inner();

    Ok(invoice)
}

Para poder utilizar estas funciones debemos decirle al proyecto que lightning.rs existe, para ello vamos a src/main.rs y debajo de mod routes; agregamos mod lightning;.

Volvamos a nuestro archivo de rutas src/routes.rs, vamos a crear una nueva ruta que utilizaremos para crear nuevas facturas lightning network para recibir pagos en Bitcoin, la nueva ruta /create_invoice.

Agregamos al inicio use crate::lightning; y debajo de la función index escribmos la nueva ruta:

#[get("/create_invoice/<description>/<amount>")]
pub async fn create_invoice(description: &str, amount: u32) -> String {
    let invoice = lightning::create_invoice(description, amount)
        .await
        .unwrap();

    invoice.payment_request
}

Solo falta un detalle más, en src/main.rs debemos agregar la nueva ruta, así que modificamos el método mount():

.mount("/", routes![routes::index, routes::create_invoice])

Al abrir http://localhost:8000/create_invoice/factura/999 veremos una cadena de texto que comienza por lnbc..., felicidades!!! tu app ya puede interactual con Lightning Network.

Retornando JSON desde rocket.rs

La ruta /create_invoice nos retorna la factura, pero para verificar el pago vamos a necesitar el hash de la factura, esto lo podemos obtener fácilmente de la struct AddInvoiceResponse, crearemos una nueva struct que utilizaremos para retornar un json que contenga la factura y el hash, para esto utilizamos serde.

Agregamos serde a nuestro proyecto:

cargo add serde

En src/routes.rs usamos serde al inicio del proyecto y agregamos una nueva struct InvoiceResponse:

use rocket::serde::{json::Json, Serialize};

#[derive(Serialize, Default)]
pub struct InvoiceResponse {
    payment_request: String,
    hash: String,
    paid: bool,
    preimage: String,
    description: String,
}

Modificamos /create_invoice para retornar json en lugar de string:

#[get("/create_invoice/<description>/<amount>")]
pub async fn create_invoice(description: &str, amount: u32) -> Json<InvoiceResponse> {
    let invoice = lightning::create_invoice(description, amount)
        .await
        .unwrap();

    let hash_str = invoice
        .r_hash
        .iter()
        .map(|h| format!("{h:02x}"))
        .collect::<Vec<String>>()
        .join("");

    Json(InvoiceResponse {
        payment_request: invoice.payment_request,
        hash: hash_str,
        ..Default::default()
    })
}

Diseccionando la Factura Lightning

Para entender la factura que acabamos de generar podemos ir a https://www.bolt11.org, pegarla en la página y ver la metadata incluida en la misma, todo el detalle lo podemos encontrar en el bolt11.

Preparamos la vista

Creamos el directorio templates en la raiz del proyecto y dentro el archivo layout.html.hbs:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  <title>Lightning Network Tipping app</title>
</head>
<body>
  <div class="container">
    {{~> content}}
  </div>
  <footer class="footer"></footer>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
  {{~> scripts}}
</body>
</html>

Hasta ahora la estructura de archivos es la siguiente:

.
├── src
│    ├── routes.rs
│    ├── lightning.rs
│    └── main.rs
├── templates
│    └── layout.html.hbs
├── Cargo.lock
├── Cargo.toml
└── .env

Creamos un formulario para generar la factura lightning

Para recibir un pago en lightning network necesitamos un formulario donde el usuario indica el monto y la descripción, dentro del directorio templates creamos un archivo llamado index.html.hbs que contenga:

{{#*inline "content"}}
    <div id="form" class="mt-5 mb-5 collapse">
      <form autocomplete="off" class="row mt-3 g-3" id="form">
        <div class="col">
          <div class="form-floating mb-3">
            <input type="text" id="description" class="form-control">
            <label for="description">Descripción</label>
          </div>
          <div class="form-floating mb-3">
            <input type="text" id="amount" class="form-control" required>
            <label for="amount">Monto</label>
          </div>

          <div class="mb-3">
            <button type="button" id="send-btn" class="btn btn-light btn-lg">Enviar</button>
          </div>
        </div>
      </form>
    </div>
    <div id="invoice" class="mt-5 mb-5 collapse bg-light rounded-3 jumbotron">
      <h3 id="invoice-memo">LNTip</h3>
      <h3><span id="invoice-amount">0</span> SATs</h3>
      <p class="lead">Para continuar, haga un pago con una billetera con soporte Bitcoin Lightning Network a la siguiente factura.</p>
      <p class="lead">Esta factura expira en 10 minutos.</p>
      <hr class="my-4">
      <h6>Factura:</h6>
      <p id="invoice-text" class="text-break"></p>
    </div>

    <div id="success-box" class="mt-5 mb-5 collapse bg-light rounded-3 jumbotron">
      <div align="center">
        <iframe src="https://giphy.com/embed/BzyTuYCmvSORqs1ABM" width="480" height="480" frameBorder="0" class="giphy-embed" allowFullScreen></iframe>
      </div>
      <hr class="my-4">
      <p class="lead" align="center">¡Pago realizado correctamente! 😀</p>
      <hr class="my-4">
    </div>
{{/inline}}
{{#*inline "scripts"}}
    <script src="/public/main.js"></script>
{{/inline}}
{{~> layout~}}

En cargo.toml agregamos la dependencia para utilizar el template engine handlebars con rocket:

[dependencies.rocket_dyn_templates]
version = "0.1.0-rc.2"
features = ["handlebars"]

En src/main.rs agregamos el soporte para templates:

use rocket::fs::{relative, FileServer}; // <--
use rocket_dyn_templates::Template; // <--

#[macro_use]
extern crate rocket;
mod lightning;
mod routes;

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/public", FileServer::from(relative!("static"))) // <-- Seteamos un directorio para contenido estático
        .mount("/", routes![routes::index, routes::create_invoice]) 
        .attach(Template::fairing()) // <--
}

Javascript en el frontend

La manera más directa que tenemos para interactuar con el usuario es utilizando javascript en el web browser, para esto creamos un directorio para almacenar contenido estático llamado static en el raiz del proyecto, dentro de static creamos un archivo main.js que ya estamos llamando desde el layout.html.hbs, el contenido inicial de main.js es el siguiente:

// /static/main.js
$(() => {
  $("#form").collapse("show");
  $("#send-btn").click(() => {
    console.log("!hola");
  });
});

Estructura de archivos:

.
├── src
│    ├── routes.rs
│    ├── lightning.rs
│    └── main.rs
├── templates
│    ├── index.html.hbs
│    └── layout.html.hbs
├── static
│    └── main.js
├── Cargo.lock
├── Cargo.toml
└── .env

Presionamos el botón Enviar y si todo está bien podremos ver en la cónsola un mensaje !hola, con esto ya podemos modificar este evento para que envíe la información a nuestra API, el archivo debe verse así:

$(() => {
  $("#form").collapse("show");
  $("#send-btn").click(sendBtn);
});

const sendBtn = async () => {
  const amount = $("#amount").val();
  const description = $("#description").val();
  $.ajax({
    url: `http://localhost:8000/create_invoice/${description}/${amount}`,
    success: function (invoice) {
      console.log(invoice);
    },
    async: false,
  });
};

Actualizamos el formulario, ingresamos descripción y monto, al enviar debemos poder ver la factura y el hash en la cónsola del navegador.

Recibiendo el pago

Necesitamos saber si una factura ha sido pagada o no, vamos a crear una nueva función en src/lightning.rs en la que le solicitemos al nodo el estado actual de una factura.

use tonic_openssl_lnd::lnrpc::{AddInvoiceResponse, Invoice, PaymentHash}; // <-- agregamos PaymentHash

pub async fn get_invoice(hash: &[u8]) -> Result<Invoice, LndClientError> {
    let mut client = connect().await.unwrap();
    let invoice = client
        .lookup_invoice(PaymentHash {
            r_hash: hash.to_vec(),
            ..Default::default()
        })
        .await?
        .into_inner();

    Ok(invoice)
}

Creamos una nueva ruta que recibe el hash de la factura y consulta la función que recién hemos creado, pero antes instalamos una nueva dependencia para el manejo de hexadecimales:

cargo add hex
use hex::FromHex;
use tonic_openssl_lnd::lnrpc::invoice::InvoiceState;

#[get("/invoice/<hash>")]
pub async fn lookup_invoice(hash: &str) -> Json<InvoiceResponse> {
    let hash = <[u8; 32]>::from_hex(hash).expect("Decoding failed");
    let invoice = lightning::get_invoice(&hash).await.unwrap();
    let mut preimage = String::new();
    let mut paid = false;

    if let Some(state) = InvoiceState::from_i32(invoice.state) {
        if state == InvoiceState::Settled {
            paid = true;
            preimage = invoice
                .r_preimage
                .iter()
                .map(|h| format!("{h:02x}"))
                .collect::<Vec<String>>()
                .join("");
        }
    }
    Json(InvoiceResponse {
        paid,
        preimage,
        description: invoice.memo,
        ..Default::default()
    })
}

Para terminar la lógica en rust, agregamos la nueva ruta routes::lookup_invoice a src/main.rs como hemos hecho con las otras rutas, solo nos falta terminar el javascript.

Ahora en main.js creamos una función llamada waitPayment() que consulta si el pago ha sido realizado.

const waitPayment = async (hash) => {
  $.ajax({
    url: `http://localhost:8000/invoice/${hash}`,
    success: function (invoice) {
      if (invoice.paid) {
        console.log("pago realizado");
      }
    },
    async: false,
  });
};

Ahora nos encontramos un problema, la función 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 waitPayment() y sendBtn() incluyendo setInterval() y clearInterva(), a continuación se muestra la versión final de main.js.

let interval = null;

$(() => {
  $("#form").collapse("show");
  $("#send-btn").click(sendBtn);
});

const sendBtn = async () => {
  const amount = $("#amount").val();
  const description = $("#description").val();
  $.ajax({
    url: `http://localhost:8000/create_invoice/${description}/${amount}`,
    success: function (invoice) {
      $("#form").collapse("hide");
      $("#invoice-amount").text(amount);
      $("#invoice-text").text(invoice.payment_request);
      $("#invoice").collapse("show");
      $("#success-box").collapse("hide");
      interval = setInterval(waitPayment, 1000, invoice.hash);
    },
    async: false,
  });
};

const waitPayment = async (hash) => {
  $.ajax({
    url: `http://localhost:8000/invoice/${hash}`,
    success: function (invoice) {
      if (invoice.paid) {
        clearInterval(interval);
        interval = null;
        $("#form").collapse("hide");
        $("#invoice").collapse("hide");
        $("#success-box").collapse("show");
      }
    },
    async: false,
  });
};

Si luego de pagar la invoice puedes ver el mensaje de ¡Pago realizado correctamente! 😀 felicidades!!! lo lograste, has terminado tu primera LApp.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment