diff options
| author | Adam Soltys <[email protected]> | 2024-08-26 21:17:43 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-08-26 21:17:43 -0700 |
| commit | fe62cadd6fb4a7e49f7d59401fc74ae0411e32a7 (patch) | |
| tree | aa8f34e1692bd2d638a2c996eeadb58671d07aea /routes/payments.ts | |
| parent | 00e43ddc0bf3d6dd719fc9295d156567fa2fecfb (diff) | |
| download | coinos-server-fe62cadd6fb4a7e49f7d59401fc74ae0411e32a7.tar.gz coinos-server-fe62cadd6fb4a7e49f7d59401fc74ae0411e32a7.zip | |
typescript
Diffstat (limited to 'routes/payments.ts')
| -rw-r--r-- | routes/payments.ts | 478 |
1 files changed, 478 insertions, 0 deletions
diff --git a/routes/payments.ts b/routes/payments.ts new file mode 100644 index 00000000..1ae87916 --- /dev/null +++ b/routes/payments.ts @@ -0,0 +1,478 @@ +import config from "$config"; +import { emit } from "$lib/sockets"; +import { generate } from "$lib/invoices"; +import { v4 } from "uuid"; +import { db, g, s, t } from "$lib/db"; +import { l, err, warn } from "$lib/logging"; +import { bail, fail, getInvoice, btc, sats, SATS, getUser } from "$lib/utils"; +import { requirePin } from "$lib/auth"; +import { + completePayment, + decode, + debit, + credit, + types, + getNode, + build, + sendLightning, + sendOnchain, +} from "$lib/payments"; +import { mqtt1, mqtt2 } from "$lib/mqtt"; +import got from "got"; +import api from "$lib/api"; + +import lq from "$lib/liquid"; +import bc from "$lib/bitcoin"; +import ln from "$lib/ln"; + +export default { + async info({ body, user }, res) { + res.send(await ln.getinfo()); + }, + + async create({ body, user }, res) { + let { amount, hash, maxfee, fund, memo, payreq, rate, username } = body; + let balance = await g(`balance:${user.id}`); + + try { + if (await g("freeze")) fail("Problem sending payment"); + + if (typeof amount !== "undefined") { + amount = parseInt(amount); + if (amount < 0 || amount > SATS || isNaN(amount)) + fail("Invalid amount"); + } + + await requirePin({ body, user }); + + let p; + + if (payreq) { + p = await sendLightning({ user, pr: payreq, amount, maxfee, memo }); + } + + if (!p) { + if (hash) { + p = await debit({ hash, amount, memo, user }); + await credit(hash, amount, memo, user.id); + } else if (fund) { + p = await debit({ hash, amount, memo: fund, user, type: types.fund }); + await db.incrBy(`fund:${fund}`, amount); + await db.lPush(`fund:${fund}:payments`, p.id); + l("funded fund", fund); + } + } + + res.send(p); + } catch (e) { + warn(user.username, "payment failed", amount, balance, hash, payreq); + err(e.message); + bail(res, e.message); + } + }, + + async list({ user: { id }, query: { start, end, limit, offset } }, res) { + if (limit) limit = parseInt(limit); + 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) { + warn("missing payment", id); + return p; + } + 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 count = payments.length; + + let fn = (a, b) => ({ + ...a, + [b.currency]: { + tips: (a[b.currency] ? a[b.currency].tips : 0) + (b.tip || 0), + fiatTips: ( + parseFloat(a[b.currency] ? a[b.currency].fiatTips : 0) + + ((b.tip || 0) * b.rate) / SATS + ).toFixed(2), + sats: + (a[b.currency] ? a[b.currency].sats : 0) + + (b.amount || 0) + + (b.tip || 0) - + (b.fee || 0) - + (b.ourfee || 0), + fiat: ( + parseFloat(a[b.currency] ? a[b.currency].fiat : 0) + + (((b.amount || 0) + + ((b.amount > 0 ? b.tip : -b.tip) || 0) - + (b.fee || 0) - + (b.ourfee || 0)) * + b.rate) / + SATS + ).toFixed(2), + }, + }); + + let incoming = payments.filter((p) => p.amount > 0).reduce(fn, {}); + let outgoing = payments.filter((p) => p.amount < 0).reduce(fn, {}); + + if (limit) payments = payments.slice(offset, offset + limit); + + res.send({ payments, count, incoming, outgoing }); + }, + + async get({ params: { hash } }, res) { + let p = await g(`payment:${hash}`); + if (typeof p === "string") p = await g(`payment:${p}`); + if (p.type === types.internal) p.with = await g(`user:${p.ref}`); + res.send(p); + }, + + async parse({ body: { payreq }, user }, res) { + try { + let hour = 1000 * 60 * 60; + let nodes = await g("nodes"); + let { last } = nodes || {}; + + if (!last || last > Date.now() - hour) { + ({ nodes } = await ln.listnodes()); + nodes.last = Date.now(); + await s("nodes", nodes); + } + + let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); + let decoded = await ln.decodepay(payreq); + let { amount_msat, payee } = decoded; + let node = nodes.find((n) => n.nodeid === payee); + let alias = node ? node.alias : payee.substr(0, 12); + + let amount = Math.round(amount_msat / 1000); + let ourfee = Math.round(amount * config.fee); + let credit = await g(`credit:lightning:${user.id}`); + let covered = Math.min(credit, ourfee) || 0; + ourfee -= covered; + + res.send({ alias, amount, ourfee }); + } catch (e) { + bail(res, e.message); + } + }, + + async fund({ params: { name } }, res) { + let amount = await g(`fund:${name}`); + if (typeof amount === "undefined" || amount === null) + return bail(res, "fund not found"); + let payments = (await db.lRange(`fund:${name}:payments`, 0, -1)) || []; + payments = await Promise.all(payments.map((hash) => g(`payment:${hash}`))); + + await Promise.all( + payments.map(async (p) => (p.user = await g(`user:${p.uid}`))), + ); + + payments = payments.filter((p) => p); + res.send({ amount, payments }); + }, + + async withdraw({ params: { name } }, res) { + let maxWithdrawable = await g(`fund:${name}`); + res.send({ + tag: "withdrawRequest", + callback: `${URL}/lnurlw/${name}`, + k1: name, + defaultDescription: `Withdraw from coinos fund ${name}`, + minWithdrawable: 0, + maxWithdrawable, + }); + }, + + async take({ body: { id, amount, invoice: iid }, user }, res) { + try { + amount = parseInt(amount); + if (amount < 0) fail("Invalid amount"); + + await t(`fund:${id}`, async (balance, db) => { + if (balance < amount) fail("Insufficient funds"); + await db.multi().decrBy(`fund:${id}`, amount).exec(); + }); + + if (!iid) { + iid = v4(); + let rates = await g("rates"); + let { currency } = user; + await s(`invoice:${iid}`, { + currency, + id: iid, + hash: iid, + rate: rates[currency], + uid: user.id, + received: 0, + }); + } + + let payment = await credit(iid, amount, id, id, types.fund); + await db.lPush(`fund:${id}:payments`, payment.id); + + res.send({ payment }); + } catch (e) { + warn("problem withdrawing from fund", e.message); + bail(res, e.message); + } + }, + + async confirm({ body: { txid, wallet, type } }, res) { + try { + let node = getNode(type); + + if (wallet === config.bitcoin.wallet || wallet === config.liquid.wallet) { + let { confirmations, details } = await node.getTransaction(txid); + for (let { address, amount, asset, category, vout } of details) { + if (!address) continue; + if (type === types.liquid && asset !== config.liquid.btc) continue; + if (category === "send") { + let p = await g(`payment:${txid}`); + if (typeof p === "string") p = await g(`payment:${p}`); + if (!p) continue; + + if (confirmations >= 1) p.confirmed = true; + await s(`payment:${p.id}`, p); + emit(p.uid, "payment", p); + continue; + } + + let p = await g(`payment:${txid}:${vout}`); + if (typeof p === "string") p = await g(`payment:${p}`); + + if (!p) { + await credit(address, sats(amount), "", `${txid}:${vout}`, type); + } else if (confirmations >= 1) { + let id = `payment:${txid}:${vout}`; + let p = await g(id); + if (typeof p === "string") p = await g(`payment:${p}`); + if (!p) return db.sAdd("missed", id); + if (p.confirmed) return; + + let invoice = await getInvoice(address); + let { id: iid } = invoice; + + p.confirmed = true; + invoice.received += parseInt(invoice.pending); + invoice.pending = 0; + + l("confirming", id, p.id, p.amount); + + let r = await db + .multi() + .set(`invoice:${iid}`, JSON.stringify(invoice)) + .set(`payment:${p.id}`, JSON.stringify(p)) + .decrBy(`pending:${p.uid}`, p.amount) + .incrBy(`balance:${p.uid}`, p.amount) + .exec(); + + emit(p.uid, "payment", p); + let user = await g(`user:${p.uid}`); + await completePayment(p, user); + } + } + } + res.send({}); + } catch (e) { + console.log(e); + warn(`problem processing ${txid}`); + bail(res, e.message); + } + }, + + async fee({ body, user }, res) { + try { + res.send(await build({ ...body, user })); + } catch (e) { + warn( + "problem estimating fee", + e.message, + user.username, + body.amount, + body.address, + ); + let msg = e.message; + if (msg.includes("500")) msg = ""; + bail(res, "Failed to prepare transaction " + msg); + } + }, + + async send({ body, user }, res) { + try { + await requirePin({ body, user }); + let { hash: txid } = await sendOnchain({ ...body, user }); + let pid = await g(`payment:${txid}`); + let p = await g(`payment:${pid}`); + + res.send({ txid }); + } catch (e) { + warn(user.username, "payment failed", e.message); + res.code(500).send(e.message); + } + }, + + async freeze({ body: { secret } }, res) { + try { + if (secret !== config.adminpass) fail("unauthorized"); + await s("freeze", true); + res.send("ok"); + } catch (e) { + console.log(e); + bail(res, e.message); + } + }, + + async print({ body: { id }, user }, res) { + try { + let p = await g(`payment:${id}`); + if (p.uid !== user.id) fail("unauthorized"); + emit(user.id, "payment", p); + + let { username } = user; + + mqtt1.publish( + username, + `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}`, + ); + + mqtt2.publish( + username, + `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}`, + ); + + res.send({ ok: true }); + } catch (e) { + bail(res, e.message); + } + }, + + async lnaddress( + { params: { lnaddress, amount, maxfee = 5000 }, body, user }, + res, + ) { + try { + lnaddress = decodeURIComponent(lnaddress); + await requirePin({ body, user }); + + let [username, domain] = lnaddress.split("@"); + let { minSendable, maxSendable, callback, metadata } = await got( + `https://${domain}/.well-known/lnurlp/${username}`, + ).json(); + + let memo = metadata["text/plain"] || ""; + if (amount * 1000 < minSendable || amount * 1000 > maxSendable) + fail("amount out of range"); + + let r = await got(`${callback}?amount=${amount * 1000}`).json(); + if (r.reason) fail(r.rason); + let { pr } = r; + let p = await sendLightning({ user, pr, amount, maxfee, memo }); + + if (!p) { + p = await debit({ hash: pr, amount, memo, user }); + await credit(pr, amount, memo, user.id); + } + + res.send(p); + } catch (e) { + console.log(e); + bail(res, e.message); + } + }, + + async gateway({ body: { short_channel_id, webhook } }, res) { + await s(short_channel_id, webhook); + res.send({ ok: true }); + }, + + async replace({ body: { id }, user }, res) { + try { + let { id: uid } = user; + let p = await g(`payment:${id}`); + if (!p) fail("Payment not found"); + if (p.uid !== user.id) fail("unauthorized"); + + let { tx, type } = await decode(p.hex); + let node = getNode(type); + + let fees = await fetch(`${api[type]}/fees/recommended`).then((r) => + r.json(), + ); + + let outputs = []; + for (let { + scriptPubKey: { address }, + value, + } of tx.vout) { + if (address && !(await node.getAddressInfo(address)).ismine) + outputs.push({ [address]: value }); + } + + let raw = await node.createRawTransaction(tx.vin, outputs); + + let newTx = await node.fundRawTransaction(raw, { + fee_rate: fees.fastestFee + 50, + replaceable: true, + subtractFeeFromOutputs: [], + }); + + let diff = sats(newTx.fee) - p.fee; + if (diff < 0) fail("fee must increase"); + + // let ourfee = Math.round(diff * config.fee) || 0; + // + // ourfee = await db.debit( + // `balance:${uid}`, + // `credit:${type}:${uid}`, + // p.amount || 0, + // 0, + // diff, + // ourfee, + // ); + + if (config[type].walletpass) + await node.walletPassphrase(config[type].walletpass, 300); + p.hex = (await node.signRawTransactionWithWallet(newTx.hex)).hex; + let r = await node.testMempoolAccept([p.hex]); + if (!r[0].allowed) fail(`transaction rejected ${p.hex}`); + warn("bump", user.username, p.hex); + // p.hash = await node.sendRawTransaction(p.hex); + // p.fee = sats(newTx.fee); + // await s(`payment:${id}`, p); + // await s(`payment:${p.hash}`, id); + // emit(uid, "payment", p); + + res.send({ ok: true }); + } catch (e) { + err("failed to bump payment", id, e.message); + bail(res, e.message); + } + }, + + async internal({ body: { username, amount }, user: sender }, res) { + let recipient = await getUser(username); + let inv = await generate({ + invoice: { amount, type: "lightning" }, + user: recipient, + sender, + }); + + let { hash } = inv; + let memo; + + let p = await debit({ hash, amount, memo, user: sender }); + await credit(hash, amount, memo, recipient.id); + + res.send(p); + }, +}; |
