aboutsummaryrefslogtreecommitdiffstats
path: root/routes/lnurl.ts
blob: 4585e0b5f53fa107826c349c3fea0481f6d2a9b5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
exports.up = function(knex) {
  return knex.schema.createTable('users', function(t) {
    t.increments('id').unsigned().primary();

    t.string('uuid').notNull().index();
    t.string('username').notNull().index();
    t.string('password').notNull();
    t.string('email').nullable().index();
    t.string('phone'
import { db, g, s } from "$lib/db";
import { generate } from "$lib/invoices";
import ln from "$lib/ln";
import { err, warn } from "$lib/logging";
import { serverPubkey } from "$lib/nostr";
import { credit } from "$lib/payments";
import { bail, fail, getInvoice, getUser } from "$lib/utils";
import { bech32 } from "bech32";
import got from "got";
import { v4 } from "uuid";

import { PaymentType } from "$lib/types";

const { URL } = process.env;
const host = URL.split("/").at(-1);
const fiveMinutes = 1000 * 60 * 5;

export default {
  async encode(req, res) {
    const {
      query: { address },
    } = req;
    const [name, domain] = address.split("@");
    const url = `https://${domain}/.well-known/lnurlp/${name
      .toLowerCase()
      .replace(/\s/g, "")}`;

    try {
      const r = await got(url).json();
      if (r.tag !== "payRequest") fail("not an ln address");
    } catch (e) {
      const m = `failed to lookup lightning address ${address}`;
      warn(m);
      return bail(res, m);
    }

    const enc = bech32.encode("lnurl", bech32.toWords(Buffer.from(url)), 20000);
    res.send(enc);
  },

  async decode(req, res) {
    const {
      query: { text },
    } = req;
    try {
      const url = Buffer.from(
        bech32.fromWords(bech32.decode(text, 20000).words),
      ).toString();

      const r = await got(url).json();
      res.send(r);
    } catch (e) {
      bail(res, e.message);
    }
  },

  async lnurlp(req, res) {
    const {
      params: { username },
      query: { minSendable = 1000, maxSendable = 100000000000 },
    } = req;
    try {
      const user = await getUser(
        username
          .replace("lightning:", "")
          .replace(/\s/g, "")
          .replace("=", "")
          .toLowerCase(),
      );

      if (!user) fail(`User ${username} not found`);
      const { id: uid } = user;

      const metadata = JSON.stringify([
        ["text/plain", `Paying ${username}@${host}`],
        ["text/identifier", `${username}@${host}`],
      ]);

      const id = v4();
      await s(`lnurl:${id}`, uid);

      res.send({
        allowsNostr: true,
        minSendable,
        maxSendable,
        metadata,
        nostrPubkey: serverPubkey,
        commentAllowed: 512,
        callback: `${URL}/api/lnurl/${id}`,
        tag: "payRequest",
      });
    } catch (e) {
      if (!e.message.includes("found"))
        warn("problem generating lnurlp request", username, e.message);
      bail(res, e.message);
    }
  },

  async lnurl(req, res) {
    const {
      params: { id },
      query: { amount, comment, nostr },
    } = req;
    try {
      const uid = await g(`lnurl:${id}`);
      const user = await getUser(uid);
      if (!user) fail("user not found");
      let { username } = user;
      username = username.replace(/\s/g, "").toLowerCase();

      const memo = comment ?? `Paying ${username}@${host}`;
      let metadata = JSON.stringify([
        ["text/plain", memo],
        ["text/identifier", `${username}@${host}`],
      ]);

      if (nostr) {
        try {
          const event = JSON.parse(decodeURIComponent(nostr));
          // TODO: validate the event
          await s(`zap:${id}`, event);
          metadata = nostr;
        } catch (e) {
          err("problem handling zap", e.message);
        }
      }

      const { id: iid, text: pr } = await generate({
        invoice: {
          amount: Math.round(amount / 1000),
          memo: metadata,
          type: PaymentType.lightning,
        },
        user,
      });

      res.send({ pr, routes: [], verify: `${URL}/api/lnurl/verify/${iid}` });
    } catch (e) {
      bail(res, e.message);
    }
  },

  async verify(req, res) {
    const {
      params: { id },
    } = req;
    const inv = await getInvoice(id);
    if (!inv) return res.send({ status: "ERROR", reason: "Not found" });

    const { hash, received, amount, preimage } = inv;
    const settled = received >= amount;

    res.send({ pr: hash, status: "OK", settled, preimage: preimage || null });
  },

  async pay(req, res) {
    const { username } = req.params;
    try {
      const user = await getUser(username);

      const invoices = await db.lRange(`${user.id}:invoices`, 0, 10);
      let invoice;
      for (const iid of invoices) {
        const i = await getInvoice(iid);
        const paid = i.amount > 0 && i.received >= i.amount;
        const old = Date.now() - i.created > fiveMinutes;
        if (i.own && !(paid || old)) {
          invoice = i;
          break;
        }
      }

      if (!invoice)
        invoice = await generate({
          invoice: {
            type: PaymentType.lightning,
          },
          user,
        });

      const { id: uid } = user;

      const metadata = JSON.stringify([
        ["text/plain", `Paying ${username}@${host}`],
        ["text/identifier", `${username}@${host}`],
      ]);

      const id = v4();
      await s(`lnurl:${id}`, uid);
      const total = (parseInt(invoice.amount) + parseInt(invoice.tip)) * 1000;

      res.send({
        allowsNostr: true,
        minSendable: total,
        maxSendable: invoice.amount ? total : undefined,
        metadata,
        nostrPubkey: serverPubkey,
        commentAllowed: 512,
        callback: `${URL}/api/lnurl/${id}`,
        tag: "payRequest",
      });
    } catch (e) {
      if (!e.message.includes("found"))
        warn("problem generating lnurlp request", username, e.message);
      bail(res, e.message);
    }
  },

  // async withdraw(req, res) {
  //   try {
  //     const { pr, k1: id } = req.query;
  //     const invoice = await getInvoice(pr);
  //     const { amount_msat } = await ln.decode(pr);
  //     const amount = Math.ceil(amount_msat / 1000);
  //     await db.debit(`fund:${id}`, "", amount, 0, 0, 0);
  //     let p;
  //     if (invoice) {
  //       const { hash } = invoice;
  //       p = await credit({
  //         hash,
  //         amount,
  //         memo: id,
  //         ref: id,
  //         type: PaymentType.fund,
  //       });
  //     } else {
  //       await ln.xpay({
  //         invstring: pr.replace(/\s/g, "").toLowerCase(),
  //         maxfee: 0,
  //         retry_for: 10,
  //       });
  //       const rates = await g("rates");
  //       p = {
  //         id: v4(),
  //         amount: -amount,
  //         hash: pr,
  //         confirmed: true,
  //         rate: rates.USD,
  //         currency: "USD",
  //         type: PaymentType.fund,
  //         ref: id,
  //         created: Date.now(),
  //       };
  //       await s(`payment:${p.id}`, p);
  //       await s(`payment:${pr}`, p);
  //     }
  //
  //     await db.lPush(`fund:${id}:payments`, p.id);
  //     res.send({ status: "OK" });
  //   } catch (e) {
  //     console.log(e);
  //     warn("lnurlw failed", e.message);
  //     res.send({ status: "ERROR", reason: "Withdrawal failed" });
  //   }
  // },
};