From 02a373b8128c1544711d14ecd6ed19e20c7e3b0b Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 27 Dec 2023 23:22:27 -0800 Subject: liquid wip --- index.js | 7 ++++--- lib/auth.js | 5 ++++- lib/db.js | 23 +++++++++++++++++++--- lib/invoices.js | 4 ++++ lib/liquid.js | 3 +++ lib/migrate.js | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/payments.js | 21 +------------------- lib/utils.js | 5 +++-- routes/payments.js | 57 ++++++++++++++++++++++++++++++++++++------------------ routes/users.js | 6 ++++++ 10 files changed, 135 insertions(+), 48 deletions(-) create mode 100644 lib/liquid.js create mode 100644 lib/migrate.js diff --git a/index.js b/index.js index 5dbfd209..43cdde73 100644 --- a/index.js +++ b/index.js @@ -67,7 +67,8 @@ app.get("/lnurlp/:username", lnurl.lnurlp); app.get("/lnurl/:id", lnurl.lnurl); app.post("/freeze", payments.freeze); -app.post("/bitcoin", payments.bitcoin); + +app.post("/confirm", payments.confirm); app.post("/bitcoin/fee", auth, payments.fee); app.post("/bitcoin/send", auth, payments.send); @@ -106,5 +107,5 @@ let port = process.env.PORT || 3119; app.listen({ host, port }); -process.on("unhandledRejection", () => err("Unhandled error")); -process.on("uncaughtException", (e) => err(e.message)); +// process.on("unhandledRejection", (e) => err("Unhandled error", e.message)); +// process.on("uncaughtException", (e) => err(e.message)); diff --git a/lib/auth.js b/lib/auth.js index 575d826e..b17a1f2d 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -1,3 +1,4 @@ +import migrate from "$lib/migrate"; import fastifyPassport from "@fastify/passport"; import { g } from "$lib/db"; import config from "$config"; @@ -36,8 +37,10 @@ export const jwtStrategy = new jwt.Strategy( async (payload, next) => { let user; if (payload.id) { - user = await g(`user:${payload.id}`); + let { id } = payload; + user = await g(`user:${id}`); if (typeof user === "string") user = await g(`user:${user}`); + if (!user) migrate(id); } next(null, user); diff --git a/lib/db.js b/lib/db.js index d14e4962..c44c7177 100644 --- a/lib/db.js +++ b/lib/db.js @@ -41,16 +41,33 @@ export let db = createClient({ }, }); -async function connectWithRetry() { +export let archive = createClient({ + url: config.archive, + socket: { + reconnectStrategy: (retries) => Math.min(retries * 50, 5000), + }, +}); + +async function dbReconnect() { try { await db.connect(); } catch (err) { console.error("Failed to connect to Redis, retrying...", err); - setTimeout(connectWithRetry, 5000); // Retry after 5 seconds + setTimeout(dbReconnect, 5000); // Retry after 5 seconds + } +} + +async function archiveReconnect() { + try { + await archive.connect(); + } catch (err) { + console.error("Failed to connect to Redis, retrying...", err); + setTimeout(archiveReconnect, 5000); // Retry after 5 seconds } } -connectWithRetry(); +dbReconnect(); +archiveReconnect(); db.on("error", (e) => { if (e.message.startsWith("getaddr")) return; diff --git a/lib/invoices.js b/lib/invoices.js index 23af1aa4..b5718e23 100644 --- a/lib/invoices.js +++ b/lib/invoices.js @@ -6,6 +6,7 @@ import { getUser, bip21, fail, SATS } from "$lib/utils"; import { types } from "$lib/payments"; import { v4 } from "uuid"; +import lq from "$lib/liquid"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; @@ -54,6 +55,9 @@ export let generate = async ({ invoice, user, sender, memo }) => { } else if (type === types.bitcoin) { hash = await bc.getNewAddress(); text = bip21(hash, invoice); + } else if (type === types.liquid) { + hash = await lq.getNewAddress(); + text = bip21(hash, invoice); } else if (type === types.internal) { hash = id; } else { diff --git a/lib/liquid.js b/lib/liquid.js new file mode 100644 index 00000000..ec8ec7cb --- /dev/null +++ b/lib/liquid.js @@ -0,0 +1,3 @@ +import config from "$config"; +import rpc from "$lib/rpc"; +export default rpc(config.liquid); diff --git a/lib/migrate.js b/lib/migrate.js new file mode 100644 index 00000000..e8ae5993 --- /dev/null +++ b/lib/migrate.js @@ -0,0 +1,52 @@ +import { archive, db, g } from "$lib/db"; + +export default async (id) => { + let user = JSON.parse(await archive.get(`user:${id}`)); + + if (typeof user === "string") { + id = user; + user = JSON.parse(await archive.get(`user:${id}`)); + } + + if (user) { + let multi = await db + .multi() + .set(`user:${user.username}`, id) + .set(`user:${user.pubkey}`, id) + .set(`user:${id}`, JSON.stringify(user)) + .set(`${id}:contacts`, await archive.get(`${id}:contacts`)) + .set(`credit:bitcoin:${id}`, await archive.get(`credit:bitcoin:${id}`) || 0) + .set( + `credit:lightning:${id}`, + await archive.get(`credit:lightning:${id}`) || 0, + ) + .set(`${id}:lastlen`, (await archive.get(`${id}:lastlen`)) || 0) + .set(`${id}:cindex`, (await archive.get(`${id}:cindex`)) || 0) + .set(`balance:${id}`, (await archive.get(`balance:${id}`)) || 0) + .set(`pending:${id}`, (await archive.get(`pending:${id}`)) || 0); + + let payments = await archive.lRange(`${id}:payments`, 0, -1); + await Promise.all( + payments.map(async (pid) => { + let p = await archive.get(`payment:${pid}`); + multi = await multi + .lPush(`${id}:payments`, pid) + .set(`payment:${pid}`, p); + }), + ); + + let invoices = await archive.lRange(`${id}:invoices`, 0, -1); + await Promise.all( + invoices.map(async (iid) => { + let inv = await archive.get(`invoice:${iid}`); + multi = await multi + .lPush(`${id}:invoices`, iid) + .set(`invoice:${iid}`, inv); + }), + ); + + await multi.exec(); + + return user; + } +}; diff --git a/lib/payments.js b/lib/payments.js index 604a78d8..e0aff192 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -23,6 +23,7 @@ export const types = { lightning: "lightning", fund: "fund", classic: "classic", + liquid: "liquid", }; export let debit = async ({ @@ -208,26 +209,6 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => { return p; }; -export let confirm = async (address, txid, vout) => { - let id = `payment:${txid}:${vout}`; - let p = await g(id); - if (typeof p === "string") p = await g(`payment:${p}`); - if (!p) return db.sAdd("missed", id); - if (p.confirmed) return; - - p.confirmed = true; - emit(p.uid, "payment", p); - - l("confirming", txid); - - let r = await db - .multi() - .set(`payment:${p.id}`, JSON.stringify(p)) - .decrBy(`pending:${p.uid}`, p.amount) - .incrBy(`balance:${p.uid}`, p.amount) - .exec(); -}; - export let payLightning = async (user, payreq, amount, maxfee, memo) => { let p; let hash = payreq; diff --git a/lib/utils.js b/lib/utils.js index 1e12214b..0c382a0c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -27,8 +27,9 @@ export const uniq = (a, k) => [...new Map(a.map((x) => [k(x), x])).values()]; export const pick = (O, K) => K.reduce((o, k) => (typeof O[k] !== "undefined" && (o[k] = O[k]), o), {}); -export const bip21 = (address, { amount, memo, tip }) => { - let url = amount || memo ? `bitcoin:${address}?` : address; +export const bip21 = (address, { amount, memo, tip, type }) => { + let network = { liquid: "liquidnetwork", bitcoin: "bitcoin" }[type]; + let url = amount || memo ? `${network}:${address}?` : address; if (amount) url += `amount=${((amount + tip) / SATS).toFixed(8)}${memo ? "&" : ""}`; if (memo) url += `message=${memo}`; diff --git a/routes/payments.js b/routes/payments.js index ba707358..f4846a64 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -6,10 +6,11 @@ import { db, g, s, t } from "$lib/db"; import { l, err, warn } from "$lib/logging"; import { bail, fail, btc, sats, SATS } from "$lib/utils"; import { requirePin } from "$lib/auth"; -import { debit, credit, confirm, types, payLightning } from "$lib/payments"; +import { debit, credit, types, payLightning } from "$lib/payments"; import { mqtt1, mqtt2 } from "$lib/mqtt"; import got from "got"; +import lq from "$lib/liquid"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; @@ -20,8 +21,8 @@ let catchUp = async () => { for (let { txid } of txns) { try { if (seen.includes(txid)) continue; - await got.post(`http://localhost:${process.env.PORT || 3119}/bitcoin`, { - json: { txid, wallet: config.bitcoin.wallet }, + await got.post(`http://localhost:${process.env.PORT || 3119}/confirm`, { + json: { txid, wallet: config.bitcoin.wallet, type: types.bitcoin }, }); seen.push(txid); @@ -233,25 +234,36 @@ export default { } }, - async bitcoin({ body: { txid, wallet } }, res) { + async confirm({ body: { txid, wallet, type } }, res) { + let node = type === types.bitcoin ? bc : lq; try { if (wallet === config.bitcoin.wallet) { - let { confirmations, details } = await bc.getTransaction(txid); + let { confirmations, details } = await node.getTransaction(txid); for (let { address, amount, category, vout } of details) { if (category !== "receive") continue; let p = await g(`payment:${txid}:${vout}`); if (typeof p === "string") p = await g(`payment:${p}`); if (!p) { - await credit( - address, - sats(amount), - "", - `${txid}:${vout}`, - types.bitcoin, - ); + await credit(address, sats(amount), "", `${txid}:${vout}`, type); } else if (confirmations >= 1) { - await confirm(address, txid, vout); + let id = `payment:${txid}:${vout}`; + let p = await g(id); + if (typeof p === "string") p = await g(`payment:${p}`); + if (!p) return db.sAdd("missed", id); + if (p.confirmed) return; + + p.confirmed = true; + emit(p.uid, "payment", p); + + l("confirming", txid); + + let r = await db + .multi() + .set(`payment:${p.id}`, JSON.stringify(p)) + .decrBy(`pending:${p.uid}`, p.amount) + .incrBy(`balance:${p.uid}`, p.amount) + .exec(); } } } @@ -263,20 +275,27 @@ export default { } }, - async fee({ body: { amount, address, feeRate: fee_rate }, user }, res) { + async fee( + { + body: { amount, address, feeRate: fee_rate, type = types.bitcoin }, + user, + }, + res, + ) { try { + let node = type === types.bitcoin ? bc : lq; amount = parseInt(amount); let replaceable = true; let ourfee = Math.round(amount * config.fee); - let credit = await g(`credit:bitcoin:${user.id}`); + let credit = await g(`credit:${type}:${user.id}`); let covered = Math.min(credit, ourfee) || 0; ourfee -= covered; let outs = [{ [address]: btc(amount) }]; - let raw = await bc.createRawTransaction([], outs, 0, replaceable); + let raw = await node.createRawTransaction([], outs, 0, replaceable); - let tx = await bc.fundRawTransaction(raw, { + let tx = await node.fundRawTransaction(raw, { fee_rate, replaceable, subtractFeeFromOutputs: [], @@ -292,9 +311,9 @@ export default { return bail(res, "insufficient funds"); outs = [{ [address]: btc(amount - ourfee) }]; - raw = await bc.createRawTransaction([], outs, 0, replaceable); + raw = await node.createRawTransaction([], outs, 0, replaceable); - tx = await bc.fundRawTransaction(raw, { + tx = await node.fundRawTransaction(raw, { fee_rate, replaceable, subtractFeeFromOutputs: [0], diff --git a/routes/users.js b/routes/users.js index a0796d63..5ebb2878 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,4 +1,5 @@ import { g, s, db } from "$lib/db"; +import migrate from "$lib/migrate"; import config from "$config"; import store from "$lib/store"; import { @@ -278,6 +279,11 @@ export default { let uid = await g(`user:${username}`); let user = await getUser(username); + if (!user) { + user = await migrate(username); + if (user) uid = user.id; + } + if ( !user || (user.password && -- cgit v1.2.3