diff options
| -rw-r--r-- | index.js | 9 | ||||
| -rw-r--r-- | lib/lightning.js | 33 | ||||
| -rw-r--r-- | lib/payments.js | 132 | ||||
| -rw-r--r-- | lib/utils.js | 2 | ||||
| -rw-r--r-- | routes/invoices.js | 16 | ||||
| -rw-r--r-- | routes/payments.js | 220 |
6 files changed, 166 insertions, 246 deletions
@@ -44,17 +44,14 @@ app.get("/locations", locations.list); app.get("/invoice", invoices.get); app.post("/invoice", optional, invoices.create); -app.post("/send", auth, payments.send); +app.post("/payments", auth, payments.create); app.get("/payments", auth, payments.list); app.get("/payments/:hash", auth, payments.get); -app.post("/lightning/send", auth, payments.sendLightning); +app.post("/query", auth, payments.query); app.get("/pot/:name", payments.pot); -app.post("/withdraw", auth, payments.withdraw); +app.post("/take", auth, payments.take); app.post("/bitcoin", payments.bitcoin); -app.get("/voucher/:id", payments.voucher); -app.post("/redeem", optional, payments.redeem); - app.get("/me", auth, users.me); app.get("/users/:key", users.get); app.post("/register", users.create); diff --git a/lib/lightning.js b/lib/lightning.js index 86dfdb2b..c8daf8c8 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -5,6 +5,7 @@ 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 { credit, types } from "$lib/payments"; export async function listenForLightning() { let { @@ -33,37 +34,7 @@ export async function listenForLightning() { let amount = parseInt(received) - tip; if (amount < 0) throw new Error("amount out of range"); - let user = await g(`user:${uid}`); - - let payment = { - amount, - created: Date.now(), - currency, - fee: 0, - hash, - memo, - preimage, - rate, - tip, - type: "lightning", - uid - }; - - invoice.status = "paid"; - invoice.received += received; - - await s(`invoice:${hash}`, invoice); - await db.incrBy(`balance:${uid}`, received); - payment.invoice = invoice; - await s(`payment:${hash}`, payment); - await db.lPush(`${uid}:payments`, hash); - - callWebhook(invoice, payment); - - emit(uid, "payment", payment); - notify(user, `Received ${received} SAT`); - - l("lightning payment received", user.username, payment.amount, payment.tip); + await credit(hash, amount, memo, preimage, types.lightning); } catch (e) { console.log(e); err("problem receiving lightning payment", e.message); 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(); +}; diff --git a/lib/utils.js b/lib/utils.js index e653f235..b65f179a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -23,7 +23,7 @@ export const fail = msg => { }; export const SATS = 100000000; -export const toSats = n => Math.round(n * SATS); +export const sats = n => Math.round(n * SATS); export const uniq = (a, k) => [...new Map(a.map(x => [k(x), x])).values()]; export const pick = (O, K) => diff --git a/routes/invoices.js b/routes/invoices.js index 1287de88..62106f3a 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -3,6 +3,8 @@ import { l } from "$lib/logging"; import { emit } from "$lib/sockets"; import { db, g, s } from "$lib/db"; import { bip21, fail } from "$lib/utils"; +import { types } from "$lib/payments"; + import bc from "$lib/bitcoin"; import ln from "$lib/ln"; @@ -31,13 +33,13 @@ export default { if (amount < 0) fail("invalid amount"); let hash, text; - if (type === "lightning") { + if (type === types.lightning) { let amt = amount ? `${amount + tip}sat` : "any"; let r = await ln.invoice(amt, new Date(), "", 3600); hash = r.payment_hash; text = r.bolt11; - } else if (type === "bitcoin") { + } else if (type === types.bitcoin) { hash = await bc.getNewAddress(); text = bip21(hash, invoice); } else { @@ -57,15 +59,7 @@ export default { created: Date.now() }; - - l( - "creating invoice", - user.username, - amount, - tip, - currency, - invoice.hash - ); + l("creating invoice", user.username, amount, tip, currency, invoice.hash); await s(`invoice:${hash}`, invoice); await db.lPush(`${uid}:invoices`, hash); diff --git a/routes/payments.js b/routes/payments.js index fc506685..d044f1e8 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -2,97 +2,19 @@ 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, warn } from "$lib/logging"; -import { fail } from "$lib/utils"; -import ln from "$lib/ln"; +import { l, err } from "$lib/logging"; +import { fail, sats } from "$lib/utils"; import { requirePin } from "$lib/auth"; -import { callWebhook } from "$lib/webhooks"; - -const { HOSTNAME: hostname } = process.env; - -let debit = async (user, amount, memo, to) => { - 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 hash = v4(); - - let p = { - hash, - amount: -amount, - memo, - uid, - rate: store.rates[currency], - currency, - type: "internal", - 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); +import { debit, credit, confirm, types } from "$lib/payments"; - return p; -}; - -let credit = async (hash, amount, memo, from) => { - let invoice = await g(`invoice:${hash}`); - let user = await g(`user:${invoice.uid}`); - - let { id: uid, currency, username } = user; - from = from.id; - - let p = { - hash, - amount, - uid, - rate: store.rates[currency], - currency, - memo, - type: memo === "pot" ? "pot" : "internal", - from, - created: Date.now() - }; - - let { tip } = invoice; - if (tip) { - p.tip = tip; - p.amount -= tip; - } - - invoice.received += amount; - await s(`invoice:${hash}`, invoice); - await s(`payment:${hash}`, p); - await db.lPush(`${uid}:payments`, hash); - - l("received internal", username, amount); - emit(username, "payment", p); - notify(user, `Received ${amount} sats`); - callWebhook(invoice, p); +import bc from "$lib/bitcoin"; +import ln from "$lib/ln"; - return p; -}; +const { HOSTNAME: hostname } = process.env; export default { - async send({ body, user }, res) { + async create({ body, user }, res) { let { amount, hash, name, memo, tip } = body; await requirePin({ body, user }); @@ -101,7 +23,7 @@ export default { let p = await debit(user, amount, memo, recipient); if (hash) { - await credit(hash, amount, memo, user); + await credit(hash, amount, memo, user.id); } else { let pot = name || v4(); await db.incrBy(`pot:${pot}`, amount); @@ -126,116 +48,11 @@ export default { res.send({ payments, total: payments.length }); }, - async voucher(req, res) { - try { - const { redeemcode } = req.params; - let payment = await g(`voucher:${id}`); - - payment = payment.get({ plain: true }); - payment.redeemer = payment["with"]; - - if (!payment) fail("invalid code"); - - res.send(payment); - } catch (e) { - res.code(500).send(e.message); - } - }, - async get({ params: { hash } }, res) { res.send(await g(`payment:${hash}`)); }, - async redeem({ body: { redeemcode } }, res) { - try { - await db.transaction(async transaction => { - if (!redeemcode) fail("no code provided"); - - let { user } = req; - - const source = await db.Payment.findOne({ - where: { - redeemcode: req.body.redeemcode - }, - include: { - model: db.Account, - as: "account" - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - l("redeeming", redeemcode); - - if (!source) fail("Invalid code"); - if (source.redeemed) fail("Voucher has already been redeemed"); - let { amount } = source; - amount = -amount; - - if (!user) { - const ip = - req.headers["x-forwarded-for"] || req.connection.remoteAddress; - - user = await register( - { - username: redeemcode.substr(0, 8), - password: "" - }, - ip, - false - ); - - let payload = { username: user.username }; - let token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { - expires: new Date(Date.now() + 432000000) - }); - - return res.send({ user }); - } - - let account = await getAccount(source.account.asset, user, transaction); - let { hash, memo, confirmed, fee, network } = source; - - source.redeemed = true; - (source.with_id = user.id), await source.save({ transaction }); - - let payment = await db.Payment.create( - { - amount, - account_id: account.id, - user_id: user.id, - hash: "Voucher " + redeemcode, - memo, - rate: store.rates[user.currency], - currency: user.currency, - confirmed, - network, - received: true, - fee, - with_id: source.user_id - }, - { transaction } - ); - - await account.increment({ balance: amount }, { transaction }); - await account.reload({ transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "payment", payment); - emit(user.username, "account", account); - - res.send({ payment }); - }); - } catch (e) { - console.log(e); - err("problem redeeming", e.message); - return res.code(500).send("There was a problem redeeming the voucher"); - } - }, - - async sendLightning({ body: { payreq } }, res) { + async query({ body: { payreq } }, res) { let hour = 1000 * 60 * 60; let { last } = store.nodes; let { nodes } = store; @@ -264,7 +81,7 @@ export default { res.send({ amount, payments }); }, - async withdraw({ body: { name, amount }, user }, res) { + async take({ body: { name, amount }, user }, res) { amount = parseInt(amount); await t(`pot:${name}`, async balance => { await new Promise(r => setTimeout(r, 100)); @@ -278,14 +95,23 @@ export default { received: 0 }); - let payment = await credit(hash, amount, "pot", { id: name }); - await db.lPush(`pot:${name}:payments`, hash); + let payment = await credit(hash, amount, "", name, types.pot); + await db.lPush(`${name}:payments`, hash); res.send({ payment }); }, - async bitcoin({ body }, res) { - console.log(body); + async bitcoin({ body: { txid, wallet } }, res) { + if (wallet === config.bitcoin.wallet) { + let { confirmations, details } = await bc.getTransaction(txid); + for (let { address, amount, vout } of details) { + if (confirmations > 0) { + await confirm(address, txid, vout); + } else { + await credit(address, sats(amount), "", `${txid}:${vout}`, types.bitcoin); + } + } + } res.send({}); } }; |
