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" });
// }
// },
};