From 5264f702a2217fe8815cb5275aca14a672f6d4bb Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 19 Jan 2023 16:00:38 -0800 Subject: refactor payment types --- lib/payments.js | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 lib/payments.js (limited to 'lib/payments.js') diff --git a/lib/payments.js b/lib/payments.js new file mode 100644 index 00000000..e12f7858 --- /dev/null +++ b/lib/payments.js @@ -0,0 +1,132 @@ +import config from "$config"; +import store from "$lib/store"; +import { emit } from "$lib/sockets"; +import { v4 } from "uuid"; +import { notify } from "$lib/notifications"; +import { db, g, s, t } from "$lib/db"; +import { l, err } from "$lib/logging"; +import { fail } from "$lib/utils"; +import { callWebhook } from "$lib/webhooks"; + +export const types = { + internal: "internal", + bitcoin: "bitcoin", + lightning: "lightning", + pot: "pot" +}; + +export let debit = async ( + user, + amount, + memo, + to, + type = types.internal, + id = v4() +) => { + let { id: uid, currency, username } = user; + to = to && to.id; + + 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 p = { + id, + amount: -amount, + memo, + uid, + rate: store.rates[currency], + currency, + type, + to, + created: Date.now() + }; + + if (!to) delete p.to; + + await s(`payment:${hash}`, p); + await db.lPush(`${uid}:payments`, hash); + + l("sent internal", user.username, amount); + emit(user.id, "payment", p); + + return p; +}; + +export let credit = async (hash, amount, memo, from, type = "internal") => { + amount = parseInt(amount); + let invoice = await g(`invoice:${hash}`); + if (!invoice) return; + + let user = await g(`user:${invoice.uid}`); + + let { id: uid, currency, username } = user; + + let p = { + hash, + amount, + uid, + rate: store.rates[currency], + currency, + memo, + from, + type, + confirmed: true, + created: Date.now() + }; + + let { tip } = invoice; + if (tip) { + p.tip = tip; + p.amount -= tip; + } + + invoice.received += amount; + + let balance = "balance"; + if (type === types.bitcoin) { + let [txid, vout] = from.split(":").slice(-2); + p.id = `${txid}:${vout}`; + p.confirmed = false; + balance = "pending"; + } else { + p.id = hash; + } + + await db + .multi() + .set(`invoice:${hash}`, JSON.stringify(invoice)) + .set(`payment:${p.id}`, JSON.stringify(p)) + .lPush(`${uid}:payments`, p.id) + .incrBy(`${balance}:${uid}`, amount) + .exec(); + + l("received internal", username, amount); + emit(uid, "payment", p); + notify(user, `Received ${amount} sats`); + callWebhook(invoice, p); + + return p; +}; + +export let confirm = async (address, txid, vout) => { + let id = `payment:${txid}:${vout}`; + let p = await g(id); + p.confirmed = true; + console.log("P", p); + + await db + .multi() + .set(id, JSON.stringify(p)) + .decrBy(`pending:${p.uid}`, p.amount) + .incrBy(`balance:${p.uid}`, p.amount) + .exec(); +}; -- cgit v1.2.3 From 5dd47b9b2cb5a9359c57079205da68a675e10b45 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 20 Jan 2023 11:40:10 -0800 Subject: endpoint renaming --- docker-compose.yml | 8 ++++++++ index.js | 2 +- lib/payments.js | 29 ++++++++++++++++------------- routes/invoices.js | 4 ++++ routes/payments.js | 32 ++++++++++++++++++++++---------- 5 files changed, 51 insertions(+), 24 deletions(-) (limited to 'lib/payments.js') diff --git a/docker-compose.yml b/docker-compose.yml index 3cf2b5aa..421b6a45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,6 +48,14 @@ services: volumes: - ./data/lightningc:/root/.lightning restart: always + cld: + image: elementsproject/lightningd:latest + container_name: cld + environment: + LIGHTNINGD_NETWORK: regtest + volumes: + - ./data/lightningd:/root/.lightning + restart: always db: image: eqalpha/keydb container_name: db diff --git a/index.js b/index.js index 7f97a20d..31739f8a 100644 --- a/index.js +++ b/index.js @@ -47,7 +47,7 @@ app.post("/invoice", optional, invoices.create); app.post("/payments", auth, payments.create); app.get("/payments", auth, payments.list); app.get("/payments/:hash", auth, payments.get); -app.post("/query", auth, payments.query); +app.post("/parse", payments.parse); app.get("/pot/:name", payments.pot); app.post("/take", auth, payments.take); app.post("/bitcoin", payments.bitcoin); diff --git a/lib/payments.js b/lib/payments.js index e12f7858..af08cb84 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -16,15 +16,20 @@ export const types = { }; export let debit = async ( - user, + hash, amount, memo, - to, + user, type = types.internal, id = v4() ) => { + console.log("HASH", hash) + let ref; + let invoice = await g(`invoice:${hash}`); + if (invoice) ref = invoice.uid; + console.log("REF", ref) + let { id: uid, currency, username } = user; - to = to && to.id; amount = parseInt(amount); @@ -46,22 +51,20 @@ export let debit = async ( rate: store.rates[currency], currency, type, - to, + ref, created: Date.now() }; - if (!to) delete p.to; - - await s(`payment:${hash}`, p); - await db.lPush(`${uid}:payments`, hash); + await s(`payment:${id}`, p); + await db.lPush(`${uid}:payments`, id); - l("sent internal", user.username, amount); + l("sent", type, user.username, amount); emit(user.id, "payment", p); return p; }; -export let credit = async (hash, amount, memo, from, type = "internal") => { +export let credit = async (hash, amount, memo, ref, type = types.internal) => { amount = parseInt(amount); let invoice = await g(`invoice:${hash}`); if (!invoice) return; @@ -77,7 +80,7 @@ export let credit = async (hash, amount, memo, from, type = "internal") => { rate: store.rates[currency], currency, memo, - from, + ref, type, confirmed: true, created: Date.now() @@ -93,7 +96,7 @@ export let credit = async (hash, amount, memo, from, type = "internal") => { let balance = "balance"; if (type === types.bitcoin) { - let [txid, vout] = from.split(":").slice(-2); + let [txid, vout] = ref.split(":").slice(-2); p.id = `${txid}:${vout}`; p.confirmed = false; balance = "pending"; @@ -109,7 +112,7 @@ export let credit = async (hash, amount, memo, from, type = "internal") => { .incrBy(`${balance}:${uid}`, amount) .exec(); - l("received internal", username, amount); + l("received", type, username, amount); emit(uid, "payment", p); notify(user, `Received ${amount} sats`); callWebhook(invoice, p); diff --git a/routes/invoices.js b/routes/invoices.js index 62106f3a..8fbb25c1 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -4,6 +4,7 @@ import { emit } from "$lib/sockets"; import { db, g, s } from "$lib/db"; import { bip21, fail } from "$lib/utils"; import { types } from "$lib/payments"; +import { v4 } from "uuid"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; @@ -33,6 +34,7 @@ export default { if (amount < 0) fail("invalid amount"); let hash, text; + console.log("TYPE", type) if (type === types.lightning) { let amt = amount ? `${amount + tip}sat` : "any"; let r = await ln.invoice(amt, new Date(), "", 3600); @@ -42,6 +44,8 @@ export default { } else if (type === types.bitcoin) { hash = await bc.getNewAddress(); text = bip21(hash, invoice); + } else if (type === types.internal) { + hash = v4(); } else { fail("unrecognized type"); } diff --git a/routes/payments.js b/routes/payments.js index d044f1e8..b92a6f81 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -18,16 +18,14 @@ export default { let { amount, hash, name, memo, tip } = body; await requirePin({ body, user }); - let invoice, recipient; - - let p = await debit(user, amount, memo, recipient); + let p = await debit(hash, amount, memo, user); if (hash) { await credit(hash, amount, memo, user.id); } else { let pot = name || v4(); await db.incrBy(`pot:${pot}`, amount); - await db.lPush(`pot:${pot}:payments`, p.hash); + await db.lPush(`pot:${pot}:payments`, p.id); l("funded pot", pot); } @@ -43,8 +41,13 @@ export default { // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; - payments = await Promise.all(payments.map(hash => g(`payment:${hash}`))); - payments = payments.filter(p => p); + payments = await Promise.all( + payments.map(async id => { + let p = await g(`payment:${id}`); + if (p.type === types.internal) p.with = await g(`user:${p.ref}`); + return p; + }) + ); res.send({ payments, total: payments.length }); }, @@ -52,7 +55,7 @@ export default { res.send(await g(`payment:${hash}`)); }, - async query({ body: { payreq } }, res) { + async parse({ body: { payreq } }, res) { let hour = 1000 * 60 * 60; let { last } = store.nodes; let { nodes } = store; @@ -61,11 +64,14 @@ export default { store.nodes = nodes; let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); - let { msatoshi, payee } = await ln.decodepay(payreq); + let decoded = await ln.decodepay(payreq); + let { msatoshi, payee, routes } = decoded; let node = nodes.find(n => n.nodeid === payee); let alias = node ? node.alias : payee.substr(0, 12); + let r = await ln.getroute(payee, msatoshi, 5); + console.log(routes, r); - res.send({ alias, amount: Math.round(msatoshi / 1000) }); + res.send({ alias, amount: Math.round(msatoshi / 1000), routes }); }, async pot({ params: { name } }, res) { @@ -108,7 +114,13 @@ export default { if (confirmations > 0) { await confirm(address, txid, vout); } else { - await credit(address, sats(amount), "", `${txid}:${vout}`, types.bitcoin); + await credit( + address, + sats(amount), + "", + `${txid}:${vout}`, + types.bitcoin + ); } } } -- cgit v1.2.3 From 2edcafb31e954628689e769c6926e3e48ddf84eb Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 22 Jan 2023 21:04:41 -0800 Subject: on chain send --- index.js | 3 ++ lib/payments.js | 9 ++--- lib/utils.js | 1 + routes/info.js | 2 +- routes/invoices.js | 1 - routes/payments.js | 112 ++++++++++++++++++++++++++++++++++++++++++++++++----- 6 files changed, 111 insertions(+), 17 deletions(-) (limited to 'lib/payments.js') diff --git a/index.js b/index.js index 31739f8a..99058833 100644 --- a/index.js +++ b/index.js @@ -50,7 +50,10 @@ app.get("/payments/:hash", auth, payments.get); app.post("/parse", payments.parse); app.get("/pot/:name", payments.pot); app.post("/take", auth, payments.take); + app.post("/bitcoin", payments.bitcoin); +app.post("/bitcoin/fee", auth, payments.fee); +app.post("/bitcoin/send", auth, payments.send); app.get("/me", auth, users.me); app.get("/users/:key", users.get); diff --git a/lib/payments.js b/lib/payments.js index af08cb84..d25df227 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -18,16 +18,15 @@ export const types = { export let debit = async ( hash, amount, + fee = 0, memo, user, type = types.internal, id = v4() ) => { - console.log("HASH", hash) let ref; let invoice = await g(`invoice:${hash}`); if (invoice) ref = invoice.uid; - console.log("REF", ref) let { id: uid, currency, username } = user; @@ -41,13 +40,13 @@ export let debit = async ( return balance - amount; }); - let fee = 0; - let p = { id, amount: -amount, + fee, memo, uid, + confirmed: true, rate: store.rates[currency], currency, type, @@ -102,7 +101,7 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => { balance = "pending"; } else { p.id = hash; - } + } await db .multi() diff --git a/lib/utils.js b/lib/utils.js index b65f179a..e58f7d1f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -24,6 +24,7 @@ export const fail = msg => { export const SATS = 100000000; export const sats = n => Math.round(n * SATS); +export const btc = n => parseFloat((n / SATS).toFixed(8)); export const uniq = (a, k) => [...new Map(a.map(x => [k(x), x])).values()]; export const pick = (O, K) => diff --git a/routes/info.js b/routes/info.js index 00a4915e..017e473a 100644 --- a/routes/info.js +++ b/routes/info.js @@ -15,7 +15,7 @@ export default { for await (let keys of stream) { for (let k of keys) b += parseInt(await db.get(k)); } - console.log(b) + console.log(b); const funds = await ln.listfunds(); lnchannel = parseInt(funds.channels.reduce((a, b) => a + b.channel_sat, 0)); diff --git a/routes/invoices.js b/routes/invoices.js index 8fbb25c1..5da481e1 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -34,7 +34,6 @@ export default { if (amount < 0) fail("invalid amount"); let hash, text; - console.log("TYPE", type) if (type === types.lightning) { let amt = amount ? `${amount + tip}sat` : "any"; let r = await ln.invoice(amt, new Date(), "", 3600); diff --git a/routes/payments.js b/routes/payments.js index b92a6f81..80d8818f 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -4,7 +4,7 @@ import { emit } from "$lib/sockets"; import { v4 } from "uuid"; import { db, g, s, t } from "$lib/db"; import { l, err } from "$lib/logging"; -import { fail, sats } from "$lib/utils"; +import { fail, btc, sats } from "$lib/utils"; import { requirePin } from "$lib/auth"; import { debit, credit, confirm, types } from "$lib/payments"; @@ -15,15 +15,41 @@ const { HOSTNAME: hostname } = process.env; export default { async create({ body, user }, res) { - let { amount, hash, name, memo, tip } = body; - await requirePin({ body, user }); + let { amount, hash, maxfee, name, memo, payreq, tip } = body; + + amount = parseInt(amount); + maxfee = parseInt(maxfee); + tip = parseInt(tip); - let p = await debit(hash, amount, memo, user); + await requirePin({ body, user }); - if (hash) { + let p; + + if (payreq) { + p = await debit( + hash, + amount + maxfee, + maxfee, + memo, + user, + types.lightning + ); + + let r = await ln.pay(payreq); + + p.amount = -amount; + p.hash = r.payment_hash; + p.fee = r.msatoshi_sent - r.msatoshi; + p.preimage = r.payment_preimage; + + await s(`payment:${p.id}`, p); + await db.incrBy(`balance:${p.uid}`, maxfee - p.fee); + } else if (hash) { + p = await debit(hash, amount, 0, memo, user); await credit(hash, amount, memo, user.id); } else { let pot = name || v4(); + p = await debit(hash, amount, 0, memo, user); await db.incrBy(`pot:${pot}`, amount); await db.lPush(`pot:${pot}:payments`, p.id); l("funded pot", pot); @@ -65,13 +91,11 @@ export default { let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); let decoded = await ln.decodepay(payreq); - let { msatoshi, payee, routes } = decoded; + let { msatoshi, payee } = decoded; let node = nodes.find(n => n.nodeid === payee); let alias = node ? node.alias : payee.substr(0, 12); - let r = await ln.getroute(payee, msatoshi, 5); - console.log(routes, r); - res.send({ alias, amount: Math.round(msatoshi / 1000), routes }); + res.send({ alias, amount: Math.round(msatoshi / 1000) }); }, async pot({ params: { name } }, res) { @@ -90,7 +114,6 @@ export default { async take({ body: { name, amount }, user }, res) { amount = parseInt(amount); await t(`pot:${name}`, async balance => { - await new Promise(r => setTimeout(r, 100)); if (balance < amount) fail("Insufficient funds"); return balance - amount; }); @@ -125,5 +148,74 @@ export default { } } res.send({}); + }, + + async fee(req, res) { + let { amount, address, feeRate, subtract } = req.body; + let subtractFeeFromOutputs = subtract ? [0] : []; + let replaceable = true; + let outs = [{ [address]: btc(amount) }]; + let count = await bc.getBlockCount(); + + let raw = await bc.createRawTransaction([], outs, 0, replaceable); + let tx = await bc.fundRawTransaction(raw, { + feeRate, + subtractFeeFromOutputs, + replaceable + }); + + if (config.bitcoin.walletpass) + await bc.walletPassphrase(config.bitcoin.walletpass, 300); + + let { hex } = await bc.signRawTransactionWithWallet(tx.hex); + let { vsize } = await bc.decodeRawTransaction(hex); + feeRate = Math.round((sats(tx.fee) * 1000) / vsize); + + res.send({ feeRate, tx }); + }, + + async send(req, res) { + await requirePin(req); + + let { user } = req; + let { address, memo, tx } = req.body; + let { hex, fee } = tx; + + if (config.bitcoin.walletpass) + await bc.walletPassphrase(config.bitcoin.walletpass, 300); + + ({ hex } = await bc.signRawTransactionWithWallet(hex)); + + let r = await bc.testMempoolAccept([hex]); + if (!r[0].allowed) fail("transaction rejected"); + + fee = sats(fee); + if (fee < 0) fail("fee cannot be negative"); + + tx = await bc.decodeRawTransaction(hex); + let { txid } = tx; + + let total = 0; + let change = 0; + + for (let { + scriptPubKey: { address }, + value + } of tx.vout) { + total += sats(value); + if ( + (await bc.getAddressInfo(address)).ismine && + !(await g(`invoice:${address}`)) + ) + change += sats(value); + } + + total = total - change + fee; + let amount = total - fee; + + await debit(txid, amount, fee, null, user, types.bitcoin, txid); + await bc.sendRawTransaction(hex); + + res.send({ txid }); } }; -- cgit v1.2.3 From f24aec1fb7dd4080d961c26ab5216fd2dfe5d971 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 24 Jan 2023 09:53:51 -0800 Subject: lnurl endpoints --- index.js | 3 +++ lib/payments.js | 1 - routes/payments.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) (limited to 'lib/payments.js') diff --git a/index.js b/index.js index 99058833..b2796203 100644 --- a/index.js +++ b/index.js @@ -50,6 +50,9 @@ app.get("/payments/:hash", auth, payments.get); app.post("/parse", payments.parse); app.get("/pot/:name", payments.pot); app.post("/take", auth, payments.take); +app.get("/encode", payments.encode); +app.get("/decode", payments.decode); +app.get("/lnurl/:code", payments.lnurl); app.post("/bitcoin", payments.bitcoin); app.post("/bitcoin/fee", auth, payments.fee); diff --git a/lib/payments.js b/lib/payments.js index d25df227..386d84d8 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -123,7 +123,6 @@ export let confirm = async (address, txid, vout) => { let id = `payment:${txid}:${vout}`; let p = await g(id); p.confirmed = true; - console.log("P", p); await db .multi() diff --git a/routes/payments.js b/routes/payments.js index 3ec196cd..53bb6e4d 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -7,6 +7,8 @@ import { l, err } from "$lib/logging"; import { fail, btc, sats } from "$lib/utils"; import { requirePin } from "$lib/auth"; import { debit, credit, confirm, types } from "$lib/payments"; +import { bech32 } from "bech32"; +import got from "got"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; @@ -217,5 +219,33 @@ export default { await bc.sendRawTransaction(hex); res.send({ txid }); + }, + + async encode({ query: { address } }, res) { + let [name, domain] = address.split("@"); + let url = `https://${domain}/.well-known/lnurlp/${name}`; + let r = await got(url).json(); + if (r.tag !== "payRequest") fail("not an ln address"); + let enc = bech32.encode("lnurl", bech32.toWords(Buffer.from(url)), 20000); + console.log(enc); + res.send(enc); + }, + + async decode({ query: { text } }, res) { + let url = Buffer.from( + bech32.fromWords(bech32.decode(text, 20000).words) + ).toString(); + + res.send(await got(url).json()); + }, + + async lnurl({ params: { code }, query: { amount } }, res) { + let { uid } = await g(`code:${code}`); + + let url = Buffer.from( + bech32.fromWords(bech32.decode(text, 20000).words) + ).toString(); + + res.send(await got(url).json()); } }; -- 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/payments.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 2eb199b8609b0976b4800815edcf73aa3da4b464 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 31 Jan 2023 20:03:04 -0800 Subject: account migration --- lib/payments.js | 2 +- lib/register.js | 1 + lib/utils.js | 20 ++++++++++++ routes/payments.js | 32 +++++++++++-------- routes/users.js | 93 ++++++++++++++++++++++++++++++++++++++++++++---------- 5 files changed, 118 insertions(+), 30 deletions(-) (limited to 'lib/payments.js') diff --git a/lib/payments.js b/lib/payments.js index d0593447..cc8747d7 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -34,7 +34,7 @@ export let debit = async ( if (!amount || amount < 0) fail("Amount must be greater than zero"); - let ourfee = Math.round(amount * config.fee); + let ourfee = type === types.internal ? 0 : 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; diff --git a/lib/register.js b/lib/register.js index e8b826e3..a3ebfc86 100644 --- a/lib/register.js +++ b/lib/register.js @@ -44,6 +44,7 @@ export default async (user, ip, requireChallenge) => { user.fiat = false; user.otpsecret = authenticator.generateSecret(); user.balance = 0; + user.migrated = true; let d = ip.split("."); ip = ((+d[0] * 256 + +d[1]) * 256 + +d[2]) * 256 + +d[3]; diff --git a/lib/utils.js b/lib/utils.js index e58f7d1f..8291d173 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -38,3 +38,23 @@ export const bip21 = (address, { amount, memo, tip }) => { return url; }; + +export const fields = [ + "cipher", + "pubkey", + "password", + "username", + "salt", + "currency", + "currencies", + "fiat", + "otpsecret", + "balance", + "ip", + "pin", + "display", + "haspin", + "profile", + "prompt", + "banner" +]; diff --git a/routes/payments.js b/routes/payments.js index ca32e969..26708a44 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -58,21 +58,27 @@ export default { async list({ user: { id }, query: { start, end, limit, offset } }, res) { if (limit) limit = parseInt(limit); - if (offset) offset = parseInt(offset); - - // if (start || end) where.createdAt = {}; - // if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); - // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); + offset = parseInt(offset) || 0; let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; - payments = await Promise.all( - payments.map(async id => { - let p = await g(`payment:${id}`); - if (p.type === types.internal) p.with = await g(`user:${p.ref}`); - return p; - }) - ); - res.send({ payments, total: payments.length }); + payments = ( + await Promise.all( + payments.map(async id => { + let p = await g(`payment:${id}`); + if (p.created < start || p.created > end) return; + if (p.type === types.internal) p.with = await g(`user:${p.ref}`); + return p; + }) + ) + ) + .filter(p => p) + .sort((a, b) => b.created - a.created); + + let total = payments.length; + + if (limit) payments = payments.slice(offset, offset + limit); + + res.send({ payments, total }); }, async get({ params: { hash } }, res) { diff --git a/routes/users.js b/routes/users.js index 49aeebd7..4b5157ce 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,7 +1,7 @@ -import { g, s } from "$lib/db"; +import { g, s, db } from "$lib/db"; import config from "$config"; import store from "$lib/store"; -import { nada, pick, uniq, wait } from "$lib/utils"; +import { fields, nada, pick, uniq, wait } from "$lib/utils"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { authenticator } from "otplib"; @@ -10,6 +10,10 @@ import { l, err, warn } from "$lib/logging"; import { emit } from "$lib/sockets"; import register from "$lib/register"; import { requirePin } from "$lib/utils"; +import { v4 } from "uuid"; +import { parseISO } from "date-fns"; +import { types } from "$lib/payments"; + import got from "got"; import upload from "$lib/upload"; @@ -172,27 +176,84 @@ export default { async login(req, res) { try { - const { params, sig, key, password, username, token: twofa } = req.body; + const { username, password, token: twofa } = req.body; l("logging in", username); - if (sig) { - const { callback } = params; + let uid = await g(`user:${username}`); + let user = await g(`user:${uid}`); + if (!(user && user.migrated)) { + let { classic } = config; try { - const url = `${callback}&sig=${sig}&key=${key}`; - const response = await got(url).json(); - res.send(response); + let { token } = await got + .post(`${classic}/login`, { json: { username, password } }) + .json(); + if (!token) fail(); + + user = await got(`${classic}/admin/migrate/${username}?zero=true`, { + headers: { authorization: `Bearer ${config.admin}` } + }).json(); + + let { balance } = user; + if (!user) fail(); + + uid = v4(); + user = { + ...pick(user, fields), + id: uid, + about: user.address, + migrated: true + }; + + await s(`user:${username}`, uid); + await s(`user:${uid}`, user); + await s(`balance:${uid}`, balance); + + let payments = await got(`${classic}/payments`, { + headers: { authorization: `Bearer ${token}` } + }).json(); + + for (let p of payments) { + let n = pick(p, ["amount", "fee", "confirmed", "rate", "currency"]); + n.id = v4(); + n.created = parseISO(p.createdAt).getTime(); + n.type = p.network; + if (!["bitcoin", "lightning"].includes(n.type)) + n.type = types.internal; + + if (n.type === types.internal) { + if (!p.with) continue; + + let id = await g(`user:${p.with.username}`); + let u = await g(`user:${id}`); + if (!u) { + u = await got(`${classic}/admin/migrate/${p.with.username}`, { + headers: { authorization: `Bearer ${config.admin}` } + }).json(); + + u = { id: v4(), about: u.address, ...pick(u, fields) }; + delete u.address; + + await s(`user:${p.with.username}`, u.id); + await s(`user:${u.id}`, u); + + l("added missing user", u.username); + } + + if (!(u && u.id)) continue; + n.ref = u.id; + } + + await s(`payment:${n.id}`, n); + await db.lPush(`${uid}:payments`, n.id); + } + + l("migrated user", user.username); } catch (e) { - err("problem calling lnurl login", e.message); - res.code(500).send(e.message); + console.log(e); } - - return; } - let id = await g(`user:${username}`); - let user = await g(`user:${id}`); - if ( !user || (user.password && !(await bcrypt.compare(password, user.password))) @@ -215,7 +276,7 @@ export default { req.headers["x-forwarded-for"] || req.socket.remoteAddress ); - let payload = { username, id: user.id }; + let payload = { username, id: uid }; let token = jwt.sign(payload, config.jwt); res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); user = pick(user, whitelist); -- cgit v1.2.3 From 1ae65a5a143d218e21828e297fd7d0ba9ab372bf Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 1 Feb 2023 11:07:37 -0800 Subject: classic invoices --- index.js | 3 ++- lib/payments.js | 8 ++++++-- routes/invoices.js | 28 ++++++++++++++++++++++++++-- routes/payments.js | 16 ++++++++++++++-- routes/users.js | 4 +++- 5 files changed, 51 insertions(+), 8 deletions(-) (limited to 'lib/payments.js') diff --git a/index.js b/index.js index 2c59b8e6..ed4e0827 100644 --- a/index.js +++ b/index.js @@ -42,8 +42,9 @@ app.post("/event", nostr.broadcast); app.get("/locations", locations.list); -app.get("/invoice", invoices.get); +app.get("/invoice/:hash", invoices.get); app.post("/invoice", optional, invoices.create); +app.get("/invoice/classic/:username", invoices.classic); app.post("/payments", auth, payments.create); app.get("/payments", auth, payments.list); diff --git a/lib/payments.js b/lib/payments.js index cc8747d7..47cb11f4 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -12,7 +12,8 @@ export const types = { internal: "internal", bitcoin: "bitcoin", lightning: "lightning", - pot: "pot" + pot: "pot", + classic: "classic" }; export let debit = async ( @@ -34,7 +35,10 @@ export let debit = async ( if (!amount || amount < 0) fail("Amount must be greater than zero"); - let ourfee = type === types.internal ? 0 : Math.round(amount * config.fee); + let ourfee = [types.bitcoin, types.lightning].includes(type) + ? 0 + : 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; diff --git a/routes/invoices.js b/routes/invoices.js index d1ec5338..2ca3b006 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -1,14 +1,38 @@ import { g, s } from "$lib/db"; import { generate } from "$lib/invoices"; +import got from "got"; +import config from "$config"; export default { - async get({ query: { hash } }, res) { + async get({ params: { hash } }, res) { let invoice = await g(`invoice:${hash}`); - invoice.user = await g(`user:${invoice.uid}`); + + if (invoice) { + invoice.user = await g(`user:${invoice.uid}`); + } else { + invoice = await got(`${config.classic}/invoice/${hash}`).json(); + invoice.id = invoice.uuid; + invoice.classic = true; + invoice.user.username += "@classic"; + } + res.send(invoice); }, async create({ body: { invoice, user }, user: sender }, res) { res.send(await generate({ invoice, user, sender })); + }, + + async classic({ params: { username } }, res) { + let invoice = await got + .post(`${config.classic}/invoice`, { + json: { + invoice: { amount: 0, network: "lightning" }, + user: { username } + } + }) + .json(); + + res.send(invoice); } }; diff --git a/routes/payments.js b/routes/payments.js index 26708a44..c67e0f9b 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -7,13 +7,14 @@ import { l, err } from "$lib/logging"; import { fail, btc, sats } from "$lib/utils"; import { requirePin } from "$lib/auth"; import { debit, credit, confirm, types } from "$lib/payments"; +import got from "got"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; export default { async create({ body, user }, res) { - let { amount, hash, maxfee, name, memo, payreq, tip } = body; + let { amount, hash, maxfee, name, memo, payreq, tip, username } = body; amount = parseInt(amount); maxfee = parseInt(maxfee); @@ -23,7 +24,18 @@ export default { let p; - if (payreq) { + if (username.endsWith("@classic")) { + let { username: source } = user; + username = username.replace("@classic", ""); + p = await debit(hash, amount, 0, memo, user, types.classic); + + await got + .post(`${config.classic}/admin/credit`, { + json: { username, amount, source }, + headers: { authorization: `Bearer ${config.admin}` } + }) + .json(); + } else if (payreq) { p = await debit( hash, amount + maxfee, diff --git a/routes/users.js b/routes/users.js index 4b5157ce..9830b2fc 100644 --- a/routes/users.js +++ b/routes/users.js @@ -183,11 +183,14 @@ export default { let user = await g(`user:${uid}`); if (!(user && user.migrated)) { + uid = user ? user.id : v4(); + let { classic } = config; try { let { token } = await got .post(`${classic}/login`, { json: { username, password } }) .json(); + if (!token) fail(); user = await got(`${classic}/admin/migrate/${username}?zero=true`, { @@ -197,7 +200,6 @@ export default { let { balance } = user; if (!user) fail(); - uid = v4(); user = { ...pick(user, fields), id: uid, -- 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/payments.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