From 91e847126c832efa25ab0bf8d6f3b7e537378fa7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 16 Jan 2023 23:40:25 -0800 Subject: shuffle things around --- lib/db.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 lib/db.js (limited to 'lib/db.js') diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 00000000..76653c81 --- /dev/null +++ b/lib/db.js @@ -0,0 +1,14 @@ +import { createClient } from "redis"; + +export const db = createClient({ + url: "redis://rd" +}); + +db.on("error", err => console.log("db error", err)); + +await db.connect(); + +export default db; + +export let g = async k => JSON.parse(await db.get(k)); +export let s = (k, v) => db.set(k, JSON.stringify(v)); -- cgit v1.2.3 From 5ae4209579287c571559edcf00dce24754553aaf Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 12:30:30 -0800 Subject: rename db --- docker-compose.yml | 2 +- lib/db.js | 17 +++++++-- lib/lightning.js | 12 +++--- lib/rates.js | 1 + lib/sockets.js | 1 + lib/utils.js | 2 +- routes/nostr.js | 2 +- routes/users.js | 107 ++++++++++++++++++++++++----------------------------- 8 files changed, 73 insertions(+), 71 deletions(-) (limited to 'lib/db.js') diff --git a/docker-compose.yml b/docker-compose.yml index 11047bf4..0555775d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: image: cs container_name: app depends_on: - - rd + - db - bc - cl command: "pnpm dev" diff --git a/lib/db.js b/lib/db.js index 76653c81..c21e224f 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,8 +1,6 @@ import { createClient } from "redis"; -export const db = createClient({ - url: "redis://rd" -}); +export const db = createClient({ url: "redis://db" }); db.on("error", err => console.log("db error", err)); @@ -12,3 +10,16 @@ export default db; export let g = async k => JSON.parse(await db.get(k)); export let s = (k, v) => db.set(k, JSON.stringify(v)); + +let retries = {}; +export let t = async (k, f) => { + retries[k] = retries[k] ? retries[k]++ : 0; + await db.watch(k); + let o = f(await g(k)); + let m = await db.multi(); + await s(k, o); + + if (retries[k] < 10) { + if (!(await m.exec())) setTimeout(() => t(k, f), 100); + } else throw new Error("could not obtain lock"); +}; diff --git a/lib/lightning.js b/lib/lightning.js index 2a8e8053..be5929b7 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -8,8 +8,8 @@ import { g, s, db } from "$lib/db"; export async function listenForLightning() { let { - payment_hash, - bolt11: request, + payment_hash: hash, + bolt11, pay_index, status, msatoshi_received, @@ -25,10 +25,10 @@ export async function listenForLightning() { if (!preimage) return; - let invoice = await g(`invoice:${payment_hash}`); - if (!invoice) return warn("received lightning with no invoice", request); + let invoice = await g(`invoice:${hash}`); + if (!invoice) return warn("received lightning with no invoice", bolt11); - let { hash, currency, memo, rate, tip, uid } = invoice; + let { currency, memo, rate, tip, uid } = invoice; let amount = parseInt(received) - tip; if (amount < 0) throw new Error("amount out of range"); @@ -50,7 +50,7 @@ export async function listenForLightning() { invoice.status = "paid"; invoice.received += received; - await s(`invoice:${payment_hash}`, invoice); + await s(`invoice:${hash}`, invoice); let t = async () => { await db.watch(`balance:${uid}`); diff --git a/lib/rates.js b/lib/rates.js index 6f87c78e..7cc24e6b 100644 --- a/lib/rates.js +++ b/lib/rates.js @@ -21,6 +21,7 @@ let connect = () => { store.last = msg.c; } catch (e) { + console.log(e) err("binance message error", e.message); } }; diff --git a/lib/sockets.js b/lib/sockets.js index e803d5b0..a7d392a9 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -83,6 +83,7 @@ export async function socketServer(app) { let { id: uid } = jwt.decode(token); if (!uid) throw new Error("Invalid JWT token"); let user = await g(`user:${uid}`); + console.log("uid", uid) if (!user) throw new Error("User not found"); if (!store.sockets[uid]) store.sockets[uid] = {}; diff --git a/lib/utils.js b/lib/utils.js index e53bf649..8d2648f2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -26,4 +26,4 @@ export const SATS = 100000000; export const toSats = n => Math.round(n * SATS); export const uniq = (a, k) => [...new Map(a.map(x => [k(x), x])).values()]; -export const pick = (O, K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); +export const pick = (O, K) => K.reduce((o, k) => (typeof O[k] !== 'undefined' && (o[k] = O[k]), o), {}); diff --git a/routes/nostr.js b/routes/nostr.js index 35b40c07..1cf9d595 100644 --- a/routes/nostr.js +++ b/routes/nostr.js @@ -1,7 +1,7 @@ import { nada, uniq } from "$lib/utils"; import config from "$config"; import { g, s, db } from "$lib/db"; -import { q } from "$lib/nostr"; +import { pool, q } from "$lib/nostr"; export default { async event({ params: { id } }, res) { diff --git a/routes/users.js b/routes/users.js index 53b02d02..c5ac61b1 100644 --- a/routes/users.js +++ b/routes/users.js @@ -117,69 +117,58 @@ export default { res.send({}); }, - async update( - { user, body: { confirm, password, pin, newpin, username } }, - res - ) { - try { - let { user } = req; - - l("updating user", user.username); - - if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); - if (typeof newpin !== "undefined") user.pin = newpin; - if (!user.pin) user.pin = null; - - let exists; - if (username) exists = await g(`user:${username}`); - - let token; - if (user.username !== username && exists) { - err("username taken", username, user.username, exists.username); - throw new Error("Username taken"); - } else if (username) { - if (user.username !== username) - l("changing username", user.username, username); - user.username = username; - } - - let attributes = [ - "address", - "cipher", - "currencies", - "currency", - "display", - "email", - "fiat", - "locktime", - "prompt", - "pubkey", - "salt", - "seed", - "tokens", - "twofa", - "unit" - ]; - - for (let a of attributes) { - if (req.body[a]) user[a] = req.body[a]; - } + async update({ user, body }, res) { + l("updating user", user.username); + + let { confirm, password, pin, newpin, username } = body; + + if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); + if (typeof newpin !== "undefined") user.pin = newpin; + if (!user.pin) user.pin = null; + + let exists; + if (username) exists = await g(`user:${username}`); + + let token; + if (user.username !== username && exists) { + err("username taken", username, user.username, exists.username); + throw new Error("Username taken"); + } else if (username) { + if (user.username !== username) + l("changing username", user.username, username); + user.username = username; + } - if (password && password === confirm) { - user.password = await bcrypt.hash(password, 1); - } + let attributes = [ + "address", + "cipher", + "currencies", + "currency", + "display", + "email", + "fiat", + "locktime", + "prompt", + "pubkey", + "salt", + "seed", + "tokens", + "twofa", + ]; + + for (let a of attributes) { + if (body[a]) user[a] = body[a]; + } - await s(`user:${user.id}`, user); + if (password && password === confirm) { + user.password = await bcrypt.hash(password, 1); + } - user.haspin = !!user.pin; + user.haspin = !!user.pin; + await s(`user:${user.id}`, user); - emit(user.username, "user", user); - res.send({ user, token }); - } catch (e) { - console.log(e); - err("error updating user", e.message); - res.code(500).send(e.message); - } + emit(user.username, "user", user); + res.send({ user, token }); }, async login(req, res) { -- cgit v1.2.3 From 33229e38a910e26083175a3e272278745378114c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 16:05:00 -0800 Subject: redlock --- index.js | 2 ++ lib/db.js | 24 +++----------- lib/lightning.js | 69 +++++++++++++++++++++++++++++++++------- lib/nostr.js | 6 ++-- lib/sendInternal.js | 4 +-- package.json | 2 ++ pnpm-lock.yaml | 65 ++++++++++++++++++++++++++++++++++++++ routes/info.js | 90 ++++++++++------------------------------------------- routes/invoices.js | 2 +- routes/nostr.js | 6 ++-- routes/payments.js | 2 +- routes/requests.js | 4 +-- 12 files changed, 158 insertions(+), 118 deletions(-) (limited to 'lib/db.js') diff --git a/index.js b/index.js index ca33b646..fd365e1a 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ import { getRates, sendRates } from "$lib/rates"; import { sendHeartbeat } from "$lib/sockets"; import email from "$routes/email"; +import info from "$routes/info"; import locations from "$routes/locations"; import nostr from "$routes/nostr"; import rates from "$routes/rates"; @@ -25,6 +26,7 @@ getRates(); setInterval(sendRates, 1000); setInterval(sendHeartbeat, 2000); +app.get("/balances", info.balances); app.post("/email", email.send); app.get("/rate", rates.last); diff --git a/lib/db.js b/lib/db.js index c21e224f..fb99a756 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,25 +1,9 @@ -import { createClient } from "redis"; - -export const db = createClient({ url: "redis://db" }); - -db.on("error", err => console.log("db error", err)); - -await db.connect(); +import { Redis } from "ioredis"; +import Redlock from "redlock"; +export const db = new Redis("redis://db", { enableAutoPipelining: true }); export default db; export let g = async k => JSON.parse(await db.get(k)); export let s = (k, v) => db.set(k, JSON.stringify(v)); - -let retries = {}; -export let t = async (k, f) => { - retries[k] = retries[k] ? retries[k]++ : 0; - await db.watch(k); - let o = f(await g(k)); - let m = await db.multi(); - await s(k, o); - - if (retries[k] < 10) { - if (!(await m.exec())) setTimeout(() => t(k, f), 100); - } else throw new Error("could not obtain lock"); -}; +export let t = (f) => new Redlock([db]).using([db], 5000, f); diff --git a/lib/lightning.js b/lib/lightning.js index be5929b7..57667be2 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -4,7 +4,8 @@ import { notify } from "$lib/notifications"; import { callWebhook } from "$lib/webhooks"; import ln from "$lib/ln"; import { l, err, warn } from "$lib/logging"; -import { g, s, db } from "$lib/db"; +import { g, s, t, db } from "$lib/db"; +import Redlock from "redlock"; export async function listenForLightning() { let { @@ -50,22 +51,66 @@ export async function listenForLightning() { invoice.status = "paid"; invoice.received += received; - await s(`invoice:${hash}`, invoice); - let t = async () => { - await db.watch(`balance:${uid}`); - let b = await g(`balance:${uid}`); - b += received; - let m = await db.multi(); - await s(`balance:${uid}`, b); - if (!(await m.exec())) t(); - }; - await t(); + const redlock = new Redlock([db]); + + await s(`invoice:${hash}`, invoice); + console.log("BAL", await g(`balance:${uid}`)); + setTimeout(async () => { + let lock = await redlock.acquire(["b"], 5000); + try { + let bal = await g(`balance:${uid}`); + console.log("b bal", bal); + await new Promise(r => setTimeout(r, 50)); + + await s(`balance:${uid}`, bal + 100); + console.log("set b bal", bal + 100); + } finally { + await lock.release(); + } + // try { + // console.log("tweaking balance"); + // await s(`balance:${uid}`, 0); + // await new Promise(r => setTimeout(r, 5)); + // console.log("mwahaha", await g(`balance:${uid}`)); + // await new Promise(r => setTimeout(r, 600)); + // console.log("hmm", await g(`balance:${uid}`)); + // } catch (e) { + // console.log(e); + // } + }, 50); + + await new Promise(r => setTimeout(r, 5)); + + let lock = await redlock.acquire(["a"], 5000); + try { + let bal = await g(`balance:${uid}`); + console.log("a bal", bal, received); + await new Promise(r => setTimeout(r, 500)); + + await s(`balance:${uid}`, bal + received); + console.log("set a bal", bal + received); + } finally { + await lock.release(); + } + + // await t(async signal => { + // console.log("acquiring lock"); + // let bal = await g(`balance:${uid}`); + // console.log("bal", bal); + // await new Promise(r => setTimeout(r, 500)); + // + // if (signal.aborted) throw signal.error; + // + // await s(`balance:${uid}`, bal + received); + // console.log("set bal", bal + received); + // }); + console.log("done", await g(`balance:${uid}`)); payment.invoice = invoice; await s(`payment:${hash}`, payment); - await db.lPush(`${uid}:payments`, hash); + await db.lpush(`${uid}:payments`, hash); callWebhook(invoice, payment); diff --git a/lib/nostr.js b/lib/nostr.js index 23ea937b..c8accf85 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -63,17 +63,17 @@ export let fillPool = () => { await s(`${pubkey}:follows`, JSON.stringify(ev.tags)); for (let f of ev.tags) { let [_, followPubkey] = f; - await db.sAdd(`${followPubkey}:followers`, pubkey); + await db.sadd(`${followPubkey}:followers`, pubkey); } coinos.send(ev); } else if (sub.includes("followers")) { let followed = sub.split(":")[0]; let { pubkey } = ev; - await db.sAdd(`${followed}:followers`, pubkey); + await db.sadd(`${followed}:followers`, pubkey); coinos.send(ev); } else if (sub.includes("notes")) { let pubkey = sub.split(":")[0]; - await db.sAdd(pubkey, ev.id); + await db.sadd(pubkey, ev.id); await s(`ev:${ev.id}`, JSON.stringify(ev)); } } catch (e) { diff --git a/lib/sendInternal.js b/lib/sendInternal.js index 32e94aa2..6054f753 100644 --- a/lib/sendInternal.js +++ b/lib/sendInternal.js @@ -39,7 +39,7 @@ export default async ({ amount, hash, memo, tip }, user) => { }; await s(`payment:${hash}`, p1); - await db.lPush(`${user.id}:payments`, hash); + await db.lpush(`${user.id}:payments`, hash); l("sent internal", user.username, -p1.amount); emit(user.id, "payment", p1); @@ -79,7 +79,7 @@ export default async ({ amount, hash, memo, tip }, user) => { invoice.received += amount; await s(`invoice:${hash}`, invoice); await s(`payment:${id}`, p2); - await db.lPush(`${recipient.id}:payments`, id); + await db.lpush(`${recipient.id}:payments`, id); l("received internal", recipient.username, amount); emit(recipient.username, "payment", p2); diff --git a/package.json b/package.json index 2c1b7258..6f1c45f9 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "fastify": "^4.5.3", "file-type": "^18.0.0", "got": "^12.5.3", + "ioredis": "^5.2.5", "jsonwebtoken": "^8.5.1", "lnurl": "^0.24.2", "nodemon": "^2.0.7", @@ -32,6 +33,7 @@ "query-string": "^6.13.1", "readable-stream-clone": "^0.0.7", "redis": "^4.3.1", + "redlock": "5.0.0-beta.2", "sharp": "^0.31.1", "uuid": "8", "web-push": "^3.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c9f09dd..4c22c13f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,7 @@ specifiers: fastify: ^4.5.3 file-type: ^18.0.0 got: ^12.5.3 + ioredis: ^5.2.5 jsonwebtoken: ^8.5.1 lnurl: ^0.24.2 nodemon: ^2.0.7 @@ -27,6 +28,7 @@ specifiers: query-string: ^6.13.1 readable-stream-clone: ^0.0.7 redis: ^4.3.1 + redlock: 5.0.0-beta.2 sharp: ^0.31.1 uuid: '8' web-push: ^3.4.4 @@ -44,6 +46,7 @@ dependencies: fastify: 4.8.1 file-type: 18.0.0 got: 12.5.3 + ioredis: 5.2.5 jsonwebtoken: 8.5.1 lnurl: 0.24.2 nodemon: 2.0.20 @@ -58,6 +61,7 @@ dependencies: query-string: 6.14.1 readable-stream-clone: 0.0.7 redis: 4.3.1 + redlock: 5.0.0-beta.2 sharp: 0.31.1 uuid: 8.3.2 web-push: 3.5.0 @@ -175,6 +179,10 @@ packages: - utf-8-validate dev: false + /@ioredis/commands/1.2.0: + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + dev: false + /@mapbox/node-pre-gyp/1.0.10: resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==} hasBin: true @@ -856,6 +864,11 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /denque/2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + /depd/2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1390,6 +1403,23 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: false + /ioredis/5.2.5: + resolution: {integrity: sha512-7HKo/ClM2DGLRXdFq8ruS3Uuadensz4A76wPOU0adqlOqd1qkhoLPDaBhmVhUhNGpB+J65/bhLmNB8DDY99HJQ==} + engines: {node: '>=12.22.0'} + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.0 + debug: 4.3.4 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + /ip/2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} dev: false @@ -1544,10 +1574,18 @@ packages: - supports-color dev: false + /lodash.defaults/4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: false + /lodash.includes/4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} dev: false + /lodash.isarguments/3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false + /lodash.isboolean/3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} dev: false @@ -1737,6 +1775,10 @@ packages: semver: 7.3.8 dev: false + /node-abort-controller/3.0.1: + resolution: {integrity: sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==} + dev: false + /node-addon-api/2.0.2: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} dev: false @@ -2129,6 +2171,18 @@ packages: engines: {node: '>= 12.13.0'} dev: false + /redis-errors/1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + dev: false + + /redis-parser/3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + dependencies: + redis-errors: 1.2.0 + dev: false + /redis/4.3.1: resolution: {integrity: sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==} dependencies: @@ -2140,6 +2194,13 @@ packages: '@redis/time-series': 1.0.3_@redis+client@1.3.0 dev: false + /redlock/5.0.0-beta.2: + resolution: {integrity: sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw==} + engines: {node: '>=12'} + dependencies: + node-abort-controller: 3.0.1 + dev: false + /require-from-string/2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2403,6 +2464,10 @@ packages: engines: {node: '>= 10.x'} dev: false + /standard-as-callback/2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false + /statuses/2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} diff --git a/routes/info.js b/routes/info.js index eb42ca35..00a4915e 100644 --- a/routes/info.js +++ b/routes/info.js @@ -1,90 +1,32 @@ -import app from "$app"; -import db from "$db"; -import config from "$config"; -import store from "$lib/store"; - -import bc from "$lib/bitcoin"; -import lq from "$lib/liquid"; +import db from "$lib/db"; import ln from "$lib/ln"; -import sequelize from "@sequelize/core"; -import { SATS } from "$lib/utils"; - -const { Op } = sequelize; - -/** - * @api {get} /info Request server information - * @apiName GetInfo - * @apiGroup Info - * - * @apiSuccess {Object} fx Exchange rates relative to USD - fx.CUR is the value of one CUR in USD. - * @apiSuccess {String[]} networks Array of supported networks; possible values are "bitcoin", "liquid" and "lightning". - * @apiSuccess {String} clientVersion the current git commit of the ui - */ -app.get("/info", async (req, res, next) => { - const { clientVersion } = config; - - const info = { - fx: store.fx, - nodes: store.networks, - clientVersion - }; - - res.send(info); -}); +export default { + async balances(req, res) { + let lnchannel; + let lnwallet; -app.get("/balances", async (req, res, next) => { - try { - const accounts = await db.Account.findAll({ - where: { - pubkey: null - }, - attributes: [ - "asset", - [sequelize.fn("sum", sequelize.col("balance")), "total"] - ], - group: ["asset"] + const stream = db.scanStream({ + match: "balance:*", + count: 100 }); - let lnchannel; - let lnwallet; + let b = 0; + for await (let keys of stream) { + for (let k of keys) b += parseInt(await db.get(k)); + } + console.log(b) const funds = await ln.listfunds(); lnchannel = parseInt(funds.channels.reduce((a, b) => a + b.channel_sat, 0)); lnwallet = parseInt(funds.outputs.reduce((a, b) => a + b.value, 0)); - let bitcoin, bitcoind; - if (config.bitcoin) { - bitcoin = parseInt((await bc.getBalance()) * SATS); - bitcoind = await bc.getNetworkInfo(); - } - - let assets, liquid, elementsd; - if (config.liquid) { - assets = await lq.getBalance(); - liquid = parseInt(assets.bitcoin * SATS); - elementsd = await lq.getNetworkInfo(); - } - const info = { - bitcoind, - elementsd, - accounts, - assets, - bitcoin, - liquid, lnchannel, - lnwallet + lnwallet, + total: lnchannel + lnwallet }; - info.total = - parseInt(bitcoin) + - parseInt(liquid) + - parseInt(lnchannel) + - parseInt(lnwallet); - res.send(info); - } catch (e) { - console.log("problem getting balances", e); } -}); +}; diff --git a/routes/invoices.js b/routes/invoices.js index acd9457e..3e43c57c 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -58,7 +58,7 @@ export default { ); await s(`invoice:${hash}`, invoice); - await db.lPush(`${uid}:invoices`, hash); + await db.lpush(`${uid}:invoices`, hash); if (request_id) { let request = await g(`request:${request_id}`); diff --git a/routes/nostr.js b/routes/nostr.js index 005a4be9..bac04810 100644 --- a/routes/nostr.js +++ b/routes/nostr.js @@ -41,10 +41,10 @@ export default { await s(`user:${pubkey}`, user); - let ids = await db.sMembers(pubkey); + let ids = await db.smembers(pubkey); let events = ids.length - ? (await db.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) + ? (await db.mget(ids.map(k => "ev:" + k))).map(JSON.parse) : []; res.send(events.map(e => ({ ...e, user }))); @@ -101,7 +101,7 @@ export default { async followers({ params: { pubkey } }, res) { try { let pubkeys = [ - ...new Set([...(await db.sMembers(`${pubkey}:followers`))]) + ...new Set([...(await db.smembers(`${pubkey}:followers`))]) ]; let followers = []; diff --git a/routes/payments.js b/routes/payments.js index 13c0cdb7..22de79c3 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -21,7 +21,7 @@ export default { // if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); - let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; + let payments = (await db.lrange(`${id}:payments`, 0, -1)) || []; payments = await Promise.all(payments.map(hash => g(`payment:${hash}`))); payments = payments.filter(p => p); res.send({ payments, total: payments.length }); diff --git a/routes/requests.js b/routes/requests.js index 79af1fb6..8db373dd 100644 --- a/routes/requests.js +++ b/routes/requests.js @@ -15,8 +15,8 @@ export default { try { const day = new Date(new Date().setDate(new Date().getDate() - 1)); - let invoices = await db.lRange(`${id}:invoices`, 0, -1); - let requests = await db.lRange(`${id}:requests`, 0, -1); + let invoices = await db.lrange(`${id}:invoices`, 0, -1); + let requests = await db.lrange(`${id}:requests`, 0, -1); res.send({ invoices, requests }); } catch (e) { -- cgit v1.2.3 From d62cc3b3db7d96ec48b71bb4adff99d134f934d7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 17 Jan 2023 23:19:56 -0800 Subject: revert redlock --- lib/db.js | 39 +++++++++++++++++++--- lib/lightning.js | 62 ++--------------------------------- lib/nostr.js | 6 ++-- lib/sendInternal.js | 93 ----------------------------------------------------- lib/whitelist.js | 2 -- package.json | 2 -- pnpm-lock.yaml | 65 ------------------------------------- routes/invoices.js | 2 +- routes/nostr.js | 4 +-- routes/payments.js | 89 ++++++++++++++++++++++++++++++++++++++++++++++---- routes/requests.js | 4 +-- routes/send.js | 15 --------- routes/users.js | 1 + 13 files changed, 129 insertions(+), 255 deletions(-) delete mode 100644 lib/sendInternal.js delete mode 100644 routes/send.js (limited to 'lib/db.js') diff --git a/lib/db.js b/lib/db.js index fb99a756..1122038c 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,9 +1,40 @@ -import { Redis } from "ioredis"; -import Redlock from "redlock"; +import { sleep } from "$lib/utils"; +import { createClient } from "redis"; -export const db = new Redis("redis://db", { enableAutoPipelining: true }); +export const db = createClient({ url: "redis://db" }); +await db.connect(); export default db; export let g = async k => JSON.parse(await db.get(k)); export let s = (k, v) => db.set(k, JSON.stringify(v)); -export let t = (f) => new Redlock([db]).using([db], 5000, f); + +let retries = {}; +export let t = async (k, f) => { + let result; + try { + result = await db.executeIsolated(async db => { + await db.watch(k); + let v = await db.get(k); + await db + .multi() + .set(k, await f(v)) + .exec(); + }); + } catch (err) { + if (!err.message.includes("watch")) throw err; + + let r = retries[k] || 0; + retries[k] = r + 1; + + if (r < 10) { + await sleep(100); + await t(k, f); + } else { + delete retries[k]; + throw new Error("unable to obtain lock"); + } + } + + delete retries[k]; + return result; +}; diff --git a/lib/lightning.js b/lib/lightning.js index 57667be2..86dfdb2b 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -5,7 +5,6 @@ import { callWebhook } from "$lib/webhooks"; import ln from "$lib/ln"; import { l, err, warn } from "$lib/logging"; import { g, s, t, db } from "$lib/db"; -import Redlock from "redlock"; export async function listenForLightning() { let { @@ -18,6 +17,7 @@ export async function listenForLightning() { } = await ln.waitanyinvoice(await g("pay_index")); await s("pay_index", pay_index); + setTimeout(listenForLightning); let received = Math.round(msatoshi_received / 1000); @@ -52,65 +52,11 @@ export async function listenForLightning() { invoice.status = "paid"; invoice.received += received; - const redlock = new Redlock([db]); - await s(`invoice:${hash}`, invoice); - console.log("BAL", await g(`balance:${uid}`)); - setTimeout(async () => { - let lock = await redlock.acquire(["b"], 5000); - try { - let bal = await g(`balance:${uid}`); - console.log("b bal", bal); - await new Promise(r => setTimeout(r, 50)); - - await s(`balance:${uid}`, bal + 100); - console.log("set b bal", bal + 100); - } finally { - await lock.release(); - } - // try { - // console.log("tweaking balance"); - // await s(`balance:${uid}`, 0); - // await new Promise(r => setTimeout(r, 5)); - // console.log("mwahaha", await g(`balance:${uid}`)); - // await new Promise(r => setTimeout(r, 600)); - // console.log("hmm", await g(`balance:${uid}`)); - // } catch (e) { - // console.log(e); - // } - }, 50); - - await new Promise(r => setTimeout(r, 5)); - - let lock = await redlock.acquire(["a"], 5000); - try { - let bal = await g(`balance:${uid}`); - console.log("a bal", bal, received); - await new Promise(r => setTimeout(r, 500)); - - await s(`balance:${uid}`, bal + received); - console.log("set a bal", bal + received); - } finally { - await lock.release(); - } - - // await t(async signal => { - // console.log("acquiring lock"); - // let bal = await g(`balance:${uid}`); - // console.log("bal", bal); - // await new Promise(r => setTimeout(r, 500)); - // - // if (signal.aborted) throw signal.error; - // - // await s(`balance:${uid}`, bal + received); - // console.log("set bal", bal + received); - // }); - console.log("done", await g(`balance:${uid}`)); - + await db.incrBy(`balance:${uid}`, received); payment.invoice = invoice; - await s(`payment:${hash}`, payment); - await db.lpush(`${uid}:payments`, hash); + await db.lPush(`${uid}:payments`, hash); callWebhook(invoice, payment); @@ -122,6 +68,4 @@ export async function listenForLightning() { console.log(e); err("problem receiving lightning payment", e.message); } - - setTimeout(listenForLightning); } diff --git a/lib/nostr.js b/lib/nostr.js index c8accf85..23ea937b 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -63,17 +63,17 @@ export let fillPool = () => { await s(`${pubkey}:follows`, JSON.stringify(ev.tags)); for (let f of ev.tags) { let [_, followPubkey] = f; - await db.sadd(`${followPubkey}:followers`, pubkey); + await db.sAdd(`${followPubkey}:followers`, pubkey); } coinos.send(ev); } else if (sub.includes("followers")) { let followed = sub.split(":")[0]; let { pubkey } = ev; - await db.sadd(`${followed}:followers`, pubkey); + await db.sAdd(`${followed}:followers`, pubkey); coinos.send(ev); } else if (sub.includes("notes")) { let pubkey = sub.split(":")[0]; - await db.sadd(pubkey, ev.id); + await db.sAdd(pubkey, ev.id); await s(`ev:${ev.id}`, JSON.stringify(ev)); } } catch (e) { diff --git a/lib/sendInternal.js b/lib/sendInternal.js deleted file mode 100644 index 6054f753..00000000 --- a/lib/sendInternal.js +++ /dev/null @@ -1,93 +0,0 @@ -import config from "$config"; -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import { callWebhook } from "$lib/webhooks"; -import { v4 } from "uuid"; -import { notify } from "$lib/notifications"; -import { l, err } from "$lib/logging"; -import { fail } from "$lib/utils"; -import ln from "$lib/ln"; -import { g, s, db } from "$lib/db"; - -const { HOSTNAME: hostname } = process.env; - -export default async ({ amount, hash, memo, tip }, user) => { - amount = parseInt(amount); - - if (!amount || amount < 0) fail("Amount must be greater than zero"); - - let fee = 0; - let invoice = await g(`invoice:${hash}`); - - let t = async () => { - await db.watch(`user:${user.id}`); - user = await g(`user:${user.id}`); - if (user.balance < amount) fail("Insufficient funds"); - user.balance -= amount; - let m = await db.multi(); - await s(`user:${user.id}`, user); - - let p1 = { - hash, - amount: -amount, - memo, - uid: user.id, - rate: store.rates[user.currency], - currency: user.currency, - type: "internal", - created: Date.now() - }; - - await s(`payment:${hash}`, p1); - await db.lpush(`${user.id}:payments`, hash); - - l("sent internal", user.username, -p1.amount); - emit(user.id, "payment", p1); - - let { uid } = invoice; - if (uid !== user.id) { - let recipient = await g(`user:${uid}`); - - let u = async () => { - await db.watch(`user:${uid}`); - recipient = await g(`user:${uid}`); - recipient.balance += amount; - let m = await db.multi(); - await s(`user:${uid}`, recipient); - if (!(await m.exec())) u(); - }; - await u(); - - let p2 = { - hash, - amount, - uid: recipient.id, - rate: store.rates[recipient.currency], - currency: recipient.currency, - memo, - type: "internal", - with_id: user.id, - created: Date.now() - }; - - let { tip } = invoice; - if (tip) { - p2.tip = tip; - p2.amount -= tip; - } - - invoice.received += amount; - await s(`invoice:${hash}`, invoice); - await s(`payment:${id}`, p2); - await db.lpush(`${recipient.id}:payments`, id); - - l("received internal", recipient.username, amount); - emit(recipient.username, "payment", p2); - notify(recipient, `Received ${amount} sats`); - callWebhook(invoice, p2); - } - - if (!(await m.exec())) t(); - }; - await t(); -}; diff --git a/lib/whitelist.js b/lib/whitelist.js index 49cc9af3..77243855 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -22,10 +22,8 @@ export default [ "pubkey", "salt", "seed", - "symbol", "theme", "twofa", - "unit", "username", "verified" ]; diff --git a/package.json b/package.json index 6f1c45f9..2c1b7258 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "fastify": "^4.5.3", "file-type": "^18.0.0", "got": "^12.5.3", - "ioredis": "^5.2.5", "jsonwebtoken": "^8.5.1", "lnurl": "^0.24.2", "nodemon": "^2.0.7", @@ -33,7 +32,6 @@ "query-string": "^6.13.1", "readable-stream-clone": "^0.0.7", "redis": "^4.3.1", - "redlock": "5.0.0-beta.2", "sharp": "^0.31.1", "uuid": "8", "web-push": "^3.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c22c13f..5c9f09dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,6 @@ specifiers: fastify: ^4.5.3 file-type: ^18.0.0 got: ^12.5.3 - ioredis: ^5.2.5 jsonwebtoken: ^8.5.1 lnurl: ^0.24.2 nodemon: ^2.0.7 @@ -28,7 +27,6 @@ specifiers: query-string: ^6.13.1 readable-stream-clone: ^0.0.7 redis: ^4.3.1 - redlock: 5.0.0-beta.2 sharp: ^0.31.1 uuid: '8' web-push: ^3.4.4 @@ -46,7 +44,6 @@ dependencies: fastify: 4.8.1 file-type: 18.0.0 got: 12.5.3 - ioredis: 5.2.5 jsonwebtoken: 8.5.1 lnurl: 0.24.2 nodemon: 2.0.20 @@ -61,7 +58,6 @@ dependencies: query-string: 6.14.1 readable-stream-clone: 0.0.7 redis: 4.3.1 - redlock: 5.0.0-beta.2 sharp: 0.31.1 uuid: 8.3.2 web-push: 3.5.0 @@ -179,10 +175,6 @@ packages: - utf-8-validate dev: false - /@ioredis/commands/1.2.0: - resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} - dev: false - /@mapbox/node-pre-gyp/1.0.10: resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==} hasBin: true @@ -864,11 +856,6 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false - /denque/2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - dev: false - /depd/2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1403,23 +1390,6 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: false - /ioredis/5.2.5: - resolution: {integrity: sha512-7HKo/ClM2DGLRXdFq8ruS3Uuadensz4A76wPOU0adqlOqd1qkhoLPDaBhmVhUhNGpB+J65/bhLmNB8DDY99HJQ==} - engines: {node: '>=12.22.0'} - dependencies: - '@ioredis/commands': 1.2.0 - cluster-key-slot: 1.1.0 - debug: 4.3.4 - denque: 2.1.0 - lodash.defaults: 4.2.0 - lodash.isarguments: 3.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - dev: false - /ip/2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} dev: false @@ -1574,18 +1544,10 @@ packages: - supports-color dev: false - /lodash.defaults/4.2.0: - resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - dev: false - /lodash.includes/4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} dev: false - /lodash.isarguments/3.1.0: - resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - dev: false - /lodash.isboolean/3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} dev: false @@ -1775,10 +1737,6 @@ packages: semver: 7.3.8 dev: false - /node-abort-controller/3.0.1: - resolution: {integrity: sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==} - dev: false - /node-addon-api/2.0.2: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} dev: false @@ -2171,18 +2129,6 @@ packages: engines: {node: '>= 12.13.0'} dev: false - /redis-errors/1.2.0: - resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} - engines: {node: '>=4'} - dev: false - - /redis-parser/3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} - dependencies: - redis-errors: 1.2.0 - dev: false - /redis/4.3.1: resolution: {integrity: sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==} dependencies: @@ -2194,13 +2140,6 @@ packages: '@redis/time-series': 1.0.3_@redis+client@1.3.0 dev: false - /redlock/5.0.0-beta.2: - resolution: {integrity: sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw==} - engines: {node: '>=12'} - dependencies: - node-abort-controller: 3.0.1 - dev: false - /require-from-string/2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2464,10 +2403,6 @@ packages: engines: {node: '>= 10.x'} dev: false - /standard-as-callback/2.1.0: - resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - dev: false - /statuses/2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} diff --git a/routes/invoices.js b/routes/invoices.js index 3e43c57c..acd9457e 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -58,7 +58,7 @@ export default { ); await s(`invoice:${hash}`, invoice); - await db.lpush(`${uid}:invoices`, hash); + await db.lPush(`${uid}:invoices`, hash); if (request_id) { let request = await g(`request:${request_id}`); diff --git a/routes/nostr.js b/routes/nostr.js index bac04810..2900e949 100644 --- a/routes/nostr.js +++ b/routes/nostr.js @@ -41,7 +41,7 @@ export default { await s(`user:${pubkey}`, user); - let ids = await db.smembers(pubkey); + let ids = await db.sMembers(pubkey); let events = ids.length ? (await db.mget(ids.map(k => "ev:" + k))).map(JSON.parse) @@ -101,7 +101,7 @@ export default { async followers({ params: { pubkey } }, res) { try { let pubkeys = [ - ...new Set([...(await db.smembers(`${pubkey}:followers`))]) + ...new Set([...(await db.sMembers(`${pubkey}:followers`))]) ]; let followers = []; diff --git a/routes/payments.js b/routes/payments.js index 22de79c3..27c8c5de 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -1,16 +1,91 @@ +import config from "$config"; +import store from "$lib/store"; import { emit } from "$lib/sockets"; -import { db, g, s } from "$lib/db"; +import { v4 } from "uuid"; +import { notify } from "$lib/notifications"; +import { db, g, s, t } from "$lib/db"; import { l, err, warn } from "$lib/logging"; import { fail } from "$lib/utils"; import ln from "$lib/ln"; -import sendInternal from "$lib/sendInternal"; import { requirePin } from "$lib/auth"; +import { callWebhook } from "$lib/webhooks"; + +const { HOSTNAME: hostname } = process.env; export default { - async send(req, res) { - await requirePin(req); - let payment = await sendInternal(req.body, req.user); - res.send(payment); + async send({ body, user }, res) { + let { amount, hash, memo, tip } = body; + await requirePin({ body, user }); + if (!hash) fail("invoice hash required"); + + let { id: uid } = user; + amount = parseInt(amount); + + if (!amount || amount < 0) fail("Amount must be greater than zero"); + + await t(`balance:${uid}`, async (balance) => { + await new Promise(r => setTimeout(r, 100)); + if (balance < amount) fail("Insufficient funds"); + return balance - amount; + }); + + let fee = 0; + let invoice = await g(`invoice:${hash}`); + user = await g(`user:${uid}`); + + await s(`user:${uid}`, user); + + let p1 = { + hash, + amount: -amount, + memo, + uid, + rate: store.rates[user.currency], + currency: user.currency, + type: "internal", + created: Date.now() + }; + + await s(`payment:${hash}`, p1); + await db.lPush(`${uid}:payments`, hash); + + l("sent internal", user.username, -p1.amount); + emit(user.id, "payment", p1); + + ({ uid } = invoice); + if (uid !== user.id) { + let recipient = await g(`user:${uid}`); + + let p2 = { + hash, + amount, + uid: recipient.id, + rate: store.rates[recipient.currency], + currency: recipient.currency, + memo, + type: "internal", + with_id: user.id, + created: Date.now() + }; + + let { tip } = invoice; + if (tip) { + p2.tip = tip; + p2.amount -= tip; + } + + invoice.received += amount; + await s(`invoice:${hash}`, invoice); + await s(`payment:${hash}`, p2); + await db.lPush(`${recipient.id}:payments`, hash); + + l("received internal", recipient.username, amount); + emit(recipient.username, "payment", p2); + notify(recipient, `Received ${amount} sats`); + callWebhook(invoice, p2); + } + + res.send(p1); }, async list({ user: { id }, query: { start, end, limit, offset } }, res) { @@ -21,7 +96,7 @@ export default { // if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); - let payments = (await db.lrange(`${id}:payments`, 0, -1)) || []; + let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; payments = await Promise.all(payments.map(hash => g(`payment:${hash}`))); payments = payments.filter(p => p); res.send({ payments, total: payments.length }); diff --git a/routes/requests.js b/routes/requests.js index 8db373dd..79af1fb6 100644 --- a/routes/requests.js +++ b/routes/requests.js @@ -15,8 +15,8 @@ export default { try { const day = new Date(new Date().setDate(new Date().getDate() - 1)); - let invoices = await db.lrange(`${id}:invoices`, 0, -1); - let requests = await db.lrange(`${id}:requests`, 0, -1); + let invoices = await db.lRange(`${id}:invoices`, 0, -1); + let requests = await db.lRange(`${id}:requests`, 0, -1); res.send({ invoices, requests }); } catch (e) { diff --git a/routes/send.js b/routes/send.js deleted file mode 100644 index 4b954616..00000000 --- a/routes/send.js +++ /dev/null @@ -1,15 +0,0 @@ -import { err } from "$lib/logging"; -import sendInternal from "$lib/sendInternal"; -import { requirePin } from "$lib/auth"; - -export default async (req, res, next) => { - try { - await requirePin(req); - let payment = await sendInternal(req.body, req.user); - res.send(payment); - } catch (e) { - console.log(e); - err("problem sending internal payment", e.message); - return res.code(500).send(e.message); - } -}; diff --git a/routes/users.js b/routes/users.js index 49aeebd7..57a423b8 100644 --- a/routes/users.js +++ b/routes/users.js @@ -19,6 +19,7 @@ export default { async me({ user }, res) { try { user.balance = await g(`balance:${user.id}`); + console.log("USER BAL", user.id, user.balance) res.send(pick(user, whitelist)); } catch (e) { console.log("problem fetching user", e); -- cgit v1.2.3 From 143490db49b6d67e9ddb2c59ea92ae923d08911c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 25 Jan 2023 20:38:10 +0000 Subject: db url in config --- lib/db.js | 3 ++- sampleconfig.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'lib/db.js') diff --git a/lib/db.js b/lib/db.js index 1122038c..acc4bff1 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,7 +1,8 @@ +import config from "$config" import { sleep } from "$lib/utils"; import { createClient } from "redis"; -export const db = createClient({ url: "redis://db" }); +export const db = createClient({ url: config.db }); await db.connect(); export default db; diff --git a/sampleconfig.js b/sampleconfig.js index b82623bc..9284866a 100644 --- a/sampleconfig.js +++ b/sampleconfig.js @@ -1,4 +1,5 @@ export default { + db: "redis://db", nostr: "ws://nostr:8080", relays: [ "ws://nostr:8080", -- cgit v1.2.3 From f0543b6d94081f43a265bb9f754a654df9c4268a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 26 Jan 2023 11:26:05 -0800 Subject: sample notify script --- lib/db.js | 12 ++---------- lib/payments.js | 31 ++++++++++++++++++++++++------- lib/upload.js | 20 +++++++++----------- lib/whitelist.js | 1 + routes/payments.js | 12 ++++++++---- sampledata/bitcoin/bitcoin.conf | 1 + sampledata/bitcoin/transaction.sh | 2 ++ 7 files changed, 47 insertions(+), 32 deletions(-) create mode 100755 sampledata/bitcoin/transaction.sh (limited to 'lib/db.js') diff --git a/lib/db.js b/lib/db.js index acc4bff1..516db236 100644 --- a/lib/db.js +++ b/lib/db.js @@ -11,16 +11,9 @@ export let s = (k, v) => db.set(k, JSON.stringify(v)); let retries = {}; export let t = async (k, f) => { - let result; try { - result = await db.executeIsolated(async db => { - await db.watch(k); - let v = await db.get(k); - await db - .multi() - .set(k, await f(v)) - .exec(); - }); + await db.watch(k); + await f(await db.get(k), db); } catch (err) { if (!err.message.includes("watch")) throw err; @@ -37,5 +30,4 @@ export let t = async (k, f) => { } delete retries[k]; - return result; }; diff --git a/lib/payments.js b/lib/payments.js index 386d84d8..d0593447 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -34,16 +34,30 @@ export let debit = async ( if (!amount || amount < 0) fail("Amount must be greater than zero"); - await t(`balance:${uid}`, async balance => { - await new Promise(r => setTimeout(r, 100)); - if (balance < amount) fail("Insufficient funds"); - return balance - amount; + let ourfee = Math.round(amount * config.fee); + await t(`balance:${uid}`, async (balance, db) => { + await t(`credit:${type}:${uid}`, async (credit, db) => { + let covered = Math.min(credit, ourfee) || 0; + ourfee -= covered; + + if (balance < amount + ourfee) fail("Insufficient funds"); + await db + .multi() + .decrBy(`credit:${type}:${uid}`, covered) + .exec(); + }); + + await db + .multi() + .decrBy(`balance:${uid}`, amount + (ourfee - credit)) + .exec(); }); let p = { id, amount: -amount, fee, + ourfee, memo, uid, confirmed: true, @@ -103,9 +117,12 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => { p.id = hash; } - await db - .multi() - .set(`invoice:${hash}`, JSON.stringify(invoice)) + let m = await db.multi(); + + if ([types.bitcoin, types.lightning].includes(type)) + m.incrBy(`credit:${type}:${uid}`, Math.round(amount * config.fee)); + + m.set(`invoice:${hash}`, JSON.stringify(invoice)) .set(`payment:${p.id}`, JSON.stringify(p)) .lPush(`${uid}:payments`, p.id) .incrBy(`${balance}:${uid}`, amount) diff --git a/lib/upload.js b/lib/upload.js index fbf9ce20..5f5aebed 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -5,12 +5,16 @@ import fs from "fs"; import path from "path"; import sharp from "sharp"; import { pipeline } from "stream"; -import { g, s } from "$lib/db"; +import { g, s, db } from "$lib/db"; -export default async ({ params: { type }, file, user }, res) => { +export default async (req, res) => { + let { + params: { type }, + user + } = req; if (!["banner", "profile"].includes(type)) throw new Error("invalid upload"); - let data = await file(); + let data = await req.file(); let s1 = new Clone(data.file); let s2 = new Clone(data.file); @@ -30,14 +34,8 @@ export default async ({ params: { type }, file, user }, res) => { await new Promise(r => pipeline(s2, t, fs.createWriteStream(path), r)); - let r = async () => { - await db.watch(`user:${user.id}`); - user = await g(`user:${user.id}`); - user[type] = true; - let m = await db.multi(); - await s(`user:${user.id}`, user); - }; - await r(); + user[type] = true; + await s(`user:${user.id}`, user); res.send(name); }; diff --git a/lib/whitelist.js b/lib/whitelist.js index 77243855..850dffc0 100644 --- a/lib/whitelist.js +++ b/lib/whitelist.js @@ -14,6 +14,7 @@ export default [ "haspin", "id", "index", + "linked", "keys", "locktime", "payments", diff --git a/routes/payments.js b/routes/payments.js index a060d90a..ca32e969 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -111,9 +111,12 @@ export default { async take({ body: { name, amount }, user }, res) { amount = parseInt(amount); - await t(`pot:${name}`, async balance => { + await t(`pot:${name}`, async (balance, db) => { if (balance < amount) fail("Insufficient funds"); - return balance - amount; + await db + .multi() + .decrBy(`pot:${name}`, amount) + .exec(); }); let hash = v4(); @@ -211,10 +214,11 @@ export default { total = total - change + fee; let amount = total - fee; + if (change && !amount) fail("Cannot send to unregistered coinos address"); + await debit(txid, amount, fee, null, user, types.bitcoin, txid); await bc.sendRawTransaction(hex); res.send({ txid }); - }, - + } }; diff --git a/sampledata/bitcoin/bitcoin.conf b/sampledata/bitcoin/bitcoin.conf index 5da37e5a..3aa44186 100644 --- a/sampledata/bitcoin/bitcoin.conf +++ b/sampledata/bitcoin/bitcoin.conf @@ -16,6 +16,7 @@ rpcwallet=coinos wallet=coinos wallet=external +walletnotify=/home/bitcoin/.bitcoin/transaction.sh %s %w zmqpubrawblock=tcp://0.0.0.0:18506 zmqpubrawtx=tcp://0.0.0.0:18507 diff --git a/sampledata/bitcoin/transaction.sh b/sampledata/bitcoin/transaction.sh new file mode 100755 index 00000000..e44789d1 --- /dev/null +++ b/sampledata/bitcoin/transaction.sh @@ -0,0 +1,2 @@ +#!/bin/sh +wget -O- --post-data='{"txid": "'$1'", "wallet": "'$2'"}' --header='Content-Type:application/json' 'http://app:3119/bitcoin' -- cgit v1.2.3 From 17a2a698de2a522f05d9ec813411b5a01b95234d Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 3 Feb 2023 09:58:47 -0800 Subject: contacts --- lib/db.js | 2 +- lib/lightning.js | 9 +++------ lib/nostr.js | 1 + lib/payments.js | 5 +++-- lib/register.js | 3 ++- lib/upload.js | 2 +- routes/payments.js | 2 +- routes/users.js | 39 +++++++++++++++++++++++++++++---------- 8 files changed, 41 insertions(+), 22 deletions(-) (limited to 'lib/db.js') diff --git a/lib/db.js b/lib/db.js index 516db236..86f8082e 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,4 +1,4 @@ -import config from "$config" +import config from "$config"; import { sleep } from "$lib/utils"; import { createClient } from "redis"; diff --git a/lib/lightning.js b/lib/lightning.js index c8daf8c8..2915b7c7 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -8,6 +8,7 @@ import { g, s, t, db } from "$lib/db"; import { credit, types } from "$lib/payments"; export async function listenForLightning() { + let event = await ln.waitanyinvoice(await g("pay_index")); let { payment_hash: hash, bolt11, @@ -15,7 +16,7 @@ export async function listenForLightning() { status, msatoshi_received, payment_preimage: preimage - } = await ln.waitanyinvoice(await g("pay_index")); + } = event; await s("pay_index", pay_index); setTimeout(listenForLightning); @@ -30,11 +31,7 @@ export async function listenForLightning() { let invoice = await g(`invoice:${hash}`); if (!invoice) return warn("received lightning with no invoice", bolt11); - let { currency, memo, rate, tip, uid } = invoice; - let amount = parseInt(received) - tip; - if (amount < 0) throw new Error("amount out of range"); - - await credit(hash, amount, memo, preimage, types.lightning); + await credit(hash, received, invoice.memo, preimage, types.lightning); } catch (e) { console.log(e); err("problem receiving lightning payment", e.message); diff --git a/lib/nostr.js b/lib/nostr.js index 23ea937b..065fafb0 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -15,6 +15,7 @@ export let fillPool = () => { }); pool.on("event", async (relay, sub, ev) => { + if (!coinos) return; if (timeouts[sub]) timeouts[sub].extend(ev); try { let content; diff --git a/lib/payments.js b/lib/payments.js index 47cb11f4..6fd595d4 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -36,8 +36,8 @@ export let debit = async ( if (!amount || amount < 0) fail("Amount must be greater than zero"); let ourfee = [types.bitcoin, types.lightning].includes(type) - ? 0 - : Math.round(amount * config.fee); + ? Math.round(amount * config.fee) + : 0; await t(`balance:${uid}`, async (balance, db) => { await t(`credit:${type}:${uid}`, async (credit, db) => { @@ -85,6 +85,7 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => { amount = parseInt(amount); let invoice = await g(`invoice:${hash}`); if (!invoice) return; + if (!memo) ({ memo } = invoice); let user = await g(`user:${invoice.uid}`); diff --git a/lib/register.js b/lib/register.js index a3ebfc86..a222a416 100644 --- a/lib/register.js +++ b/lib/register.js @@ -8,7 +8,7 @@ import { s, g } from "$lib/db"; import { got } from "got"; export default async (user, ip, requireChallenge) => { - let { password, username } = user; + let { password, pubkey, username } = user; l("registering", username); if (!username) throw new Error("Username required"); @@ -54,6 +54,7 @@ export default async (user, ip, requireChallenge) => { await s(`user:${id}`, user); await s(`user:${user.username}`, id); + await s(`user:${pubkey}`, id); l("new user", user.username); return user; diff --git a/lib/upload.js b/lib/upload.js index 2f8180a0..954e8d4d 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -12,7 +12,7 @@ export default async (req, res) => { params: { type }, user } = req; - console.log("TYPE", type) + console.log("TYPE", type); if (!["banner", "profile"].includes(type)) throw new Error("invalid upload"); let data = await req.file(); diff --git a/routes/payments.js b/routes/payments.js index 5c3944ce..970bc187 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -31,7 +31,7 @@ export default { await got .post(`${config.classic}/admin/credit`, { - json: { username, amount, source }, + json: { username, amount, source }, headers: { authorization: `Bearer ${config.admin}` } }) .json(); diff --git a/routes/users.js b/routes/users.js index e3264937..10cc358d 100644 --- a/routes/users.js +++ b/routes/users.js @@ -23,6 +23,7 @@ export default { async me({ user }, res) { try { user.balance = await g(`balance:${user.id}`); + user.prompt = !!user.prompt; res.send(pick(user, whitelist)); } catch (e) { console.log("problem fetching user", e); @@ -33,7 +34,9 @@ export default { async get({ params: { key } }, res) { try { if (key.startsWith("npub")) { - key = Buffer.from(fromWords(decode(key).words)).toString("hex"); + try { + key = Buffer.from(fromWords(decode(key).words)).toString("hex"); + } catch (e) {} } let user = await g(`user:${key}`); @@ -43,7 +46,9 @@ export default { if (!user && key.length === 64) { user = { - username: key.substr(0, 6), + currency: "USD", + username: key, + display: key.substr(0, 6), pubkey: key, anon: true }; @@ -63,6 +68,8 @@ export default { "id" ]; + user.prompt = !!user.prompt; + res.send(pick(user, whitelist)); } catch (e) { console.log(e); @@ -197,7 +204,7 @@ export default { headers: { authorization: `Bearer ${config.admin}` } }).json(); - let { balance } = user; + let { balance, pubkey } = user; if (!user) fail(); uid = user.uuid; @@ -209,6 +216,7 @@ export default { migrated: true }; + await s(`user:${pubkey}`, uid); await s(`user:${username}`, uid); await s(`user:${uid}`, user); await s(`balance:${uid}`, balance); @@ -238,6 +246,7 @@ export default { u = { id: u.uuid, about: u.address, ...pick(u, fields) }; delete u.address; + await s(`user:${u.pubkey}`, u.id); await s(`user:${p.with.username}`, u.id); await s(`user:${u.id}`, u); @@ -346,12 +355,22 @@ export default { } }, - async contacts({ user: { uuid } }, res) { - let payments = (await g(`${uuid}:payments`)) || []; - res.send( - Promise.all( - [...new Set(payments.map(p => p.with_id))].map(id => g(`user:${id}`)) - ) - ); + async contacts({ user: { id } }, res) { + let i = (await g(`${id}:cindex`)) || 0; + let payments = (await db.lRange(`${id}:payments`, i, -1)) || []; + await db.incrBy(`${id}:cindex`, payments.length); + + let contacts = (await g(`${id}:contacts`)) || []; + + for (let { ref } of ( + await Promise.all(payments.map(async id => await g(`payment:${id}`))) + ).filter(p => p.type === types.internal && p.ref)) { + !~contacts.findIndex(({ id }) => id === ref) && + contacts.push(await g(`user:${ref}`)); + } + + await s(`${id}:contacts`, contacts); + + res.send(contacts); } }; -- cgit v1.2.3