diff options
| author | Adam Soltys <[email protected]> | 2020-03-11 15:10:40 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2020-03-11 15:10:40 -0700 |
| commit | c0888fab024cc9f7c99812bfabf9d7774312dbf7 (patch) | |
| tree | cad95683af97432f6092a2c2bae10fbb7a588db5 /routes | |
| parent | bf2b51f0c2ae9b8f0f330dde6411e45fc0da8a25 (diff) | |
| download | coinos-server-c0888fab024cc9f7c99812bfabf9d7774312dbf7.tar.gz coinos-server-c0888fab024cc9f7c99812bfabf9d7774312dbf7.zip | |
refactor
Diffstat (limited to 'routes')
| -rw-r--r-- | routes/bitcoin/fee.js | 25 | ||||
| -rw-r--r-- | routes/bitcoin/receive.js | 143 | ||||
| -rw-r--r-- | routes/bitcoin/send.js | 93 | ||||
| -rw-r--r-- | routes/lightning/invoice.js | 27 | ||||
| -rw-r--r-- | routes/lightning/query.js | 7 | ||||
| -rw-r--r-- | routes/lightning/receive.js | 36 | ||||
| -rw-r--r-- | routes/lightning/send.js | 93 | ||||
| -rw-r--r-- | routes/lightning/user.js | 46 | ||||
| -rw-r--r-- | routes/liquid/fee.js | 25 | ||||
| -rw-r--r-- | routes/liquid/receive.js | 137 | ||||
| -rw-r--r-- | routes/liquid/send.js | 97 | ||||
| -rw-r--r-- | routes/payments.js | 40 | ||||
| -rw-r--r-- | routes/users.js | 328 |
13 files changed, 1097 insertions, 0 deletions
diff --git a/routes/bitcoin/fee.js b/routes/bitcoin/fee.js new file mode 100644 index 00000000..259f2329 --- /dev/null +++ b/routes/bitcoin/fee.js @@ -0,0 +1,25 @@ +module.exports = async (req, res) => { + let { user } = req; + let { address, amount, feeRate } = req.body; + let tx, fee; + + try { + amount = parseInt(amount); + + tx = await bc.createRawTransaction([], { + [address]: (amount / SATS).toFixed(8) + }); + + let params = { + subtractFeeFromOutputs: amount === user.balance ? [0] : [] + } + + if (feeRate) params["feeRate"] = (parseInt(feeRate) / SATS * 1000).toFixed(8); + + tx = await bc.fundRawTransaction(tx, params); + res.send({ tx }); + } catch (e) { + l.error("bitcoin fee estimation error", e); + return res.status(500).send(e.message); + } +}; diff --git a/routes/bitcoin/receive.js b/routes/bitcoin/receive.js new file mode 100644 index 00000000..b5ab4298 --- /dev/null +++ b/routes/bitcoin/receive.js @@ -0,0 +1,143 @@ +const reverse = require("buffer-reverse"); +const zmq = require("zeromq"); +const { Op } = require("sequelize"); + +const bitcoin = require("bitcoinjs-lib"); + +const zmqRawBlock = zmq.socket("sub"); +zmqRawBlock.connect(config.bitcoin.zmqrawblock); +zmqRawBlock.subscribe("rawblock"); + +const zmqRawTx = zmq.socket("sub"); +zmqRawTx.connect(config.bitcoin.zmqrawtx); +zmqRawTx.subscribe("rawtx"); + +let NETWORK = bitcoin.networks[config.bitcoin.network === "mainnet" ? "bitcoin" : config.bitcoin.network]; + +zmqRawTx.on("message", async (topic, message, sequence) => { + const hex = message.toString("hex"); + let tx = bitcoin.Transaction.fromHex(message); + let hash = reverse(tx.getHash()).toString("hex"); + + if (payments.includes(hash)) return; + + Promise.all( + tx.outs.map(async o => { + const { value } = o; + + let address; + try { + address = bitcoin.address.fromOutputScript( + o.script, + NETWORK + ); + } catch (e) { + return; + } + + if (Object.keys(addresses).includes(address)) { + payments.push(hash); + + let user = await db.User.findOne({ + where: { + username: addresses[address] + } + }); + + let invoices = await db.Payment.findAll({ + limit: 1, + where: { + address, + received: null, + amount: { + [Op.gt]: 0 + } + }, + order: [["createdAt", "DESC"]] + }); + + let tip = null; + if (invoices.length) tip = invoices[0].tip; + + let confirmed = false; + + user.address = await bc.getNewAddress("", "bech32"); + user.pending += value; + + await user.save(); + emit(user.username, "user", user); + + addresses[user.address] = user.username; + + const payment = await db.Payment.create({ + user_id: user.id, + hash, + amount: value, + currency: user.currency, + rate: app.get("rates")[user.currency], + received: true, + tip, + confirmed, + address, + asset: 'BTC', + }); + + l.info("bitcoin detected", user.username, o.value); + emit(user.username, "payment", payment); + } + }) + ); +}); + +let queue = {}; + +zmqRawBlock.on("message", async (topic, message, sequence) => { + const payments = await db.Payment.findAll({ + where: { confirmed: false } + }); + + const hashes = payments.map(p => p.hash); + + let block = bitcoin.Block.fromHex(message.toString("hex")); + block.transactions.map(tx => { + let hash = reverse(tx.getHash()).toString("hex"); + if (hashes.includes(hash)) queue[hash] = 1; + }); +}); + +setInterval(async () => { + let arr = Object.keys(queue); + for (let i = 0; i < arr.length; i++) { + let hash = arr[i]; + + let p = await db.Payment.findOne({ + include: [{ model: db.User, as: "user" }], + where: { hash, confirmed: 0 } + }) + + p.confirmed = 1; + + let user = await p.getUser(); + user.balance += p.amount; + user.pending -= Math.min(user.pending, p.amount); + emit(user.username, "user", user); + + await user.save(); + await p.save(); + + let payments = await db.Payment.findAll({ + where: { + user_id: user.id, + received: { + [Op.ne]: null + }, + }, + order: [['id', 'DESC']], + limit: 12 + }); + + l.info("bitcoin confirmed", user.username, p.amount); + emit(user.username, "payments", payments); + delete queue[hash]; + } +}, 1000); diff --git a/routes/bitcoin/send.js b/routes/bitcoin/send.js new file mode 100644 index 00000000..3c545178 --- /dev/null +++ b/routes/bitcoin/send.js @@ -0,0 +1,93 @@ +const bitcoin = require("bitcoinjs-lib"); +const reverse = require("buffer-reverse"); + +module.exports = async (req, res) => { + let { user } = req; + let { address, tx } = req.body; + let { hex } = tx; + + let fee = toSats(tx.fee); + + const isChange = async address => + !((await bc.getAddressInfo(address)).ismine) || + !Object.keys(addresses).includes(address) || + address === user.address; + + tx = await bc.decodeRawTransaction(hex); + + let total = 0; + let change = 0; + + for (let i = 0; i < tx.vout.length; i++) { + let o = tx.vout[i]; + total += toSats(o.value); + + if (o.scriptPubKey.addresses) { + if ((await isChange(o.scriptPubKey.addresses[0]))) { + change += toSats(o.value); + } + } + } + + total = (total - change) + fee; + let amount = total - fee; + + try { + await db.transaction(async transaction => { + let { balance } = await db.User.findOne({ + where: { + username: user.username + }, + lock: transaction.LOCK.UPDATE, + transaction + }); + + if (amount !== balance && total > balance) { + l.error("amount exceeds balance", amount, fee, balance); + throw new Error("insufficient funds"); + } + + user.balance -= total; + await user.save({ transaction }); + }); + } catch (e) { + l.warn("insufficient funds for bitcoin payment", e); + return res.status(500).send("Not enough satoshis"); + } + + try { + if (config.bitcoin.walletpass) + await bc.walletPassphrase(config.bitcoin.walletpass, 300); + + hex = (await bc.signRawTransactionWithWallet(hex)).hex; + let txid = await bc.sendRawTransaction(hex); + + await db.transaction(async transaction => { + await user.save({ transaction }); + emit(user.username, "user", user); + + const payment = await db.Payment.create( + { + amount: -total, + fee, + user_id: user.id, + hash: txid, + rate: app.get("rates")[user.currency], + currency: user.currency, + address, + confirmed: true, + received: false, + asset: "BTC" + }, + { transaction } + ); + + l.info("sent bitcoin", user.username, total); + emit(user.username, "payment", payment); + res.send(payment); + }); + } catch (e) { + l.error("error sending bitcoin", e); + return res.status(500).send(e.message); + } +}; diff --git a/routes/lightning/invoice.js b/routes/lightning/invoice.js new file mode 100644 index 00000000..59f0df47 --- /dev/null +++ b/routes/lightning/invoice.js @@ -0,0 +1,27 @@ +module.exports = async (req, res) => { + let err = m => res.status(500).send(m); + let { amount, address, tip } = req.body; + + let invoice; + try { + invoice = await lnb.addInvoice({ value: amount }); + } catch (e) { + return err(e.message); + } + + let hash = invoice.payment_request; + if (address) hash = address; + + await db.Payment.create({ + user_id: req.user.id, + hash, + amount, + currency: "CAD", + rate: app.get("rates")[req.user.currency], + tip, + confirmed: 1, + asset: 'LNBTC', + }); + + res.send(invoice); +}; diff --git a/routes/lightning/query.js b/routes/lightning/query.js new file mode 100644 index 00000000..8e25293c --- /dev/null +++ b/routes/lightning/query.js @@ -0,0 +1,7 @@ +const bolt11 = require("bolt11"); + +module.exports = async (req, res) => { + let hash = req.body.payreq; + let payreq = bolt11.decode(hash); + res.send(await lna.queryRoutes({ pub_key: payreq.payeeNodeKey, amt: payreq.satoshis })); +}; diff --git a/routes/lightning/receive.js b/routes/lightning/receive.js new file mode 100644 index 00000000..6befc7a3 --- /dev/null +++ b/routes/lightning/receive.js @@ -0,0 +1,36 @@ +module.exports = { + const handlePayment = async msg => { + if (!msg.settled) return; + + let payment = await db.Payment.findOne({ + include: { model: db.User, as: "user" }, + where: { + hash: msg.payment_request + } + }); + + if (!payment) return; + + const { user } = payment; + + payment.received = true; + user.balance += parseInt(msg.value); + payment.rate = app.get("rates")[user.currency]; + payment.confirmed = true; + payment.currency = user.currency; + + await payment.save(); + await user.save(); + payments.push(msg.payment_request); + l.info("lightning payment received", user.username, payment.amount); + + emit(user.username, "payment", payment); + emit(user.username, "user", user); + }; + + const invoices = lna.subscribeInvoices({}); + invoices.on("data", handlePayment); + + const invoicesb = lnb.subscribeInvoices({}); + invoicesb.on("data", handlePayment); +}; diff --git a/routes/lightning/send.js b/routes/lightning/send.js new file mode 100644 index 00000000..2b661b60 --- /dev/null +++ b/routes/lightning/send.js @@ -0,0 +1,93 @@ +const bolt11 = require("bolt11"); + +module.exports = async (req, res) => { + let hash = req.body.payreq; + let payreq = bolt11.decode(hash); + let { route } = req.body; + let { user } = req; + + if (seen.includes(hash)) { + l.warn("attempted to pay a paid invoice", user.username); + return res.status(500).send("Invoice has been paid, can't pay again"); + } + + try { + await db.transaction(async transaction => { + let { balance } = await db.User.findOne( + { + where: { + username: user.username + } + }, + { transaction } + ); + + if (balance < payreq.satoshis) { + throw new Error("insufficient funds"); + } + + user.balance -= payreq.satoshis; + await user.save({ transaction }); + }); + } catch (e) { + l.warn("insufficient funds for lightning payment", e); + return res.status(500).send("Not enough satoshis"); + } + + let m; + try { + let paymentHash = payreq.tags.find(t => t.tagName === 'payment_hash').data; + m = await lna.sendToRouteSync({ + "payment_hash": Buffer.from(paymentHash, 'hex'), + route + }); + + if (m.payment_error) return res.status(500).send(m.payment_error); + if (seen.includes(m.payment_preimage)) return; + seen.push(m.payment_preimage); + + let total = parseInt(m.payment_route.total_amt); + let fee = m.payment_route.total_fees; + + user.balance -= total - payreq.satoshis; + + await db.transaction(async transaction => { + await user.save({ transaction }); + + const payment = await db.Payment.create( + { + amount: -total, + fee, + user_id: user.id, + hash, + rate: app.get("rates")[user.currency], + currency: user.currency, + confirmed: true, + asset: 'LNBTC', + }, + { transaction } + ); + + l.info("sent lightning", user.username, -payment.amount); + emit(user.username, "payment", payment); + emit(user.username, "user", user); + + if (payreq.payeeNodeKey === config.lnb.id) { + lna.addInvoice({ value: payreq.satoshis }, (err, invoice) => { + let payback = lnb.sendPayment(lnb.meta, {}); + + /* eslint-disable-next-line */ + let { payment_request } = invoice; + /* eslint-disable-next-line */ + payback.write({ payment_request }); + }); + } + + seen.push(hash); + res.send(payment); + }); + } catch (e) { + l.info("error sending lightning", e); + return res.status(500).send(e); + } +}; diff --git a/routes/lightning/user.js b/routes/lightning/user.js new file mode 100644 index 00000000..c361600c --- /dev/null +++ b/routes/lightning/user.js @@ -0,0 +1,46 @@ +const bolt11 = require("bolt11"); + +module.exports = async (req, res) => { + let { payuser, amount } = req.body; + + let user = await db.User.findOne({ + where: { + username: payuser + } + }); + + if (!user) { + return res.status(500).send("Couldn't find the user you're trying to pay"); + } + let err = m => res.status(500).send(m); + + let invoice; + try { + invoice = await lnb.addInvoice({ value: amount }); + } catch (e) { + return err(e.message); + } + + let hash = invoice.payment_request; + + await db.Payment.create({ + user_id: user.id, + hash, + amount, + rate: app.get("rates")[req.user.currency], + currency: req.user.currency, + tip: 0, + confirmed: true, + received: false, + asset: 'LNBTC', + }); + + req.url = "/sendPayment"; + let payreq = bolt11.decode(hash); + const { routes } = await lna.queryRoutes({ "pub_key": payreq.payeeNodeKey, "amt": payreq.satoshis }); + if (routes.length) req.body.route = routes[0]; + else return res.status(500).send("No route available"); + req.body.payreq = hash; + + return app._router.handle(req, res); +}; diff --git a/routes/liquid/fee.js b/routes/liquid/fee.js new file mode 100644 index 00000000..402c0b2a --- /dev/null +++ b/routes/liquid/fee.js @@ -0,0 +1,25 @@ +module.exports = async (req, res) => { + let { user } = req; + let { address, amount, feeRate } = req.body; + let tx, fee; + + try { + amount = parseInt(amount); + + tx = await lq.createRawTransaction([], { + [address]: (amount / SATS).toFixed(8) + }); + + let params = { + subtractFeeFromOutputs: amount === user.balance ? [0] : [] + } + + if (feeRate) params["feeRate"] = (parseInt(feeRate) / SATS * 1000).toFixed(8); + + tx = await lq.fundRawTransaction(tx, params); + res.send({ tx }); + } catch (e) { + l.error("error estimating liquid fee", e); + return res.status(500).send(e.message); + } +}; diff --git a/routes/liquid/receive.js b/routes/liquid/receive.js new file mode 100644 index 00000000..2e622aad --- /dev/null +++ b/routes/liquid/receive.js @@ -0,0 +1,137 @@ +const reverse = require("buffer-reverse"); +const zmq = require("zeromq"); +const { Op } = require("sequelize"); +const bitcoin = require("elementsjs-lib"); + +const zmqRawBlock = zmq.socket("sub"); +zmqRawBlock.connect(config.liquid.zmqrawblock); +zmqRawBlock.subscribe("rawblock"); + +const zmqRawTx = zmq.socket("sub"); +zmqRawTx.connect("tcp://127.0.0.1:18603"); +zmqRawTx.subscribe("rawtx"); + +let NETWORK = bitcoin.networks[config.bitcoin.network === "mainnet" ? "bitcoin" : config.bitcoin.network]; + +zmqRawTx.on("message", async (topic, message, sequence) => { + const hex = message.toString("hex"); + const unblinded = await lq.unblindRawTransaction(hex); + const tx = await lq.decodeRawTransaction(unblinded.hex); + const blinded = await lq.decodeRawTransaction(hex); + if (payments.includes(blinded.txid)) return; + + Promise.all( + tx.vout.map(async o => { + if (!(o.scriptPubKey && o.scriptPubKey.addresses)) return; + const value = toSats(o.value); + const address = o.scriptPubKey.addresses[0]; + + if (Object.keys(addresses).includes(address)) { + let user = await db.User.findOne({ + where: { + username: addresses[address] + } + }); + + let invoices = await db.Payment.findAll({ + limit: 1, + where: { + address, + received: null, + amount: { + [Op.gt]: 0 + } + }, + order: [["id", "DESC"]] + }); + + let tip = null; + if (invoices.length) tip = invoices[0].tip; + + let confirmed = 0; + + user.pending += value; + user.confidential = await lq.getNewAddress(); + user.liquid = (await lq.getAddressInfo(user.confidential)).unconfidential; + + await user.save(); + emit(user.username, "user", user); + + addresses[user.liquid] = user.username; + + const payment = await db.Payment.create({ + user_id: user.id, + hash: blinded.txid, + amount: value, + currency: user.currency, + rate: app.get("rates")[user.currency], + received: true, + tip, + confirmed, + address, + asset: 'LBTC', + }); + payments.push(blinded.txid); + + l.info("liquid detected", user.username, value); + emit(user.username, "payment", payment); + } + }) + ); +}); + +let queue = {}; + +zmqRawBlock.on("message", async (topic, message, sequence) => { + const payments = await db.Payment.findAll({ + where: { confirmed: 0 } + }); + + const hashes = payments.map(p => p.hash); + + const block = bitcoin.Block.fromHex(message.toString("hex"), true); + const hash = await lq.getBlockHash(block.height); + const json = await lq.getBlock(hash, 2); + + json.tx.map(tx => { + if (hashes.includes(tx.txid)) queue[tx.txid] = 1; + }); +}); + +setInterval(async () => { + let arr = Object.keys(queue); + + for (let i = 0; i < arr.length; i++) { + let hash = arr[i]; + + let p = await db.Payment.findOne({ + include: [{ model: db.User, as: "user" }], + where: { hash, confirmed: 0, received: 1 } + }) + + p.confirmed = 1; + + let user = await p.getUser(); + user.balance += p.amount; + user.pending -= Math.min(user.pending, p.amount); + l.info("liquid confirmed", user.username, p.amount); + emit(user.username, "user", user); + + await user.save(); + await p.save(); + + let payments = await db.Payment.findAll({ + where: { + user_id: user.id, + received: { + [Op.ne]: null + }, + }, + order: [['id', 'DESC']], + limit: 12 + }); + + emit(user.username, "payments", payments); + delete queue[hash]; + } +}, 1000); diff --git a/routes/liquid/send.js b/routes/liquid/send.js new file mode 100644 index 00000000..aaced4a9 --- /dev/null +++ b/routes/liquid/send.js @@ -0,0 +1,97 @@ +const reverse = require("buffer-reverse"); + +module.exports = async (req, res) => { + let { user } = req; + let { + address, + tx: { hex } + } = req.body; + + const isChange = async address => + !((await bc.getAddressInfo(address)).ismine) || + !Object.keys(addresses).includes(address) || + address === user.liquid; + + const unblinded = await bc.unblindRawTransaction(hex); + tx = await bc.decodeRawTransaction(unblinded.hex); + + let total = 0; + let change = 0; + let fee = 0; + + for (let i = 0; i < tx.vout.length; i++) { + let o = tx.vout[i]; + total += toSats(o.value); + if (o.scriptPubKey.type === 'fee') fee = toSats(o.value); + + if (o.scriptPubKey.addresses) { + if ((await isChange(o.scriptPubKey.addresses[0]))) { + change += toSats(o.value); + } + } + } + + total = total - change; + let amount = total - fee; + + try { + await db.transaction(async transaction => { + let { balance } = await db.User.findOne({ + where: { + username: user.username + }, + lock: transaction.LOCK.UPDATE, + transaction + }); + + if (amount !== balance && total > balance) { + l.warn("amount exceeds balance", { amount, fee, balance }); + throw new Error("insufficient funds"); + } + + req.user.balance -= total; + await user.save({ transaction }); + }); + } catch (e) { + l.warn("insufficient funds for liquid payment", e); + return res.status(500).send("Not enough satoshis"); + } + + try { + if (config.liquid.walletpass) + await bc.walletPassphrase(config.liquid.walletpass, 300); + + hex = await bc.blindRawTransaction(hex); + rawtx = (await bc.signRawTransactionWithWallet(hex)).hex; + let txid = await bc.sendRawTransaction(rawtx); + + await db.transaction(async transaction => { + await user.save({ transaction }); + emit(user.username, "user", user); + l.info("total", total); + + const payment = await db.Payment.create( + { + amount: -total, + fee, + user_id: user.id, + hash: txid, + rate: app.get("rates")[user.currency], + currency: user.currency, + address, + confirmed: true, + received: false, + asset: 'LBTC' + }, + { transaction } + ); + + l.info("sent liquid", user.username, total); + emit(user.username, "payment", payment); + res.send(payment); + }); + } catch (e) { + l.error("liquid send failed", e); + return res.status(500).send(e.message); + } +}; diff --git a/routes/payments.js b/routes/payments.js new file mode 100644 index 00000000..d5038b08 --- /dev/null +++ b/routes/payments.js @@ -0,0 +1,40 @@ +const BitcoinCore = require("bitcoin-core"); +const createLnrpc = require("../lib/createLnrpc"); + +(async () => { + bc = new BitcoinCore(config.bitcoin); + lq = new BitcoinCore(config.liquid); + + lna = await createLnrpc(config.lna); + lnb = await createLnrpc(config.lnb); + + seen = []; + addresses = {}; + await db.User.findAll({ + attributes: ["username", "address", "liquid"] + }).map(u => { + addresses[u.address] = u.username; + if (u.liquid) addresses[u.liquid] = u.username; + }); + + app.post("/lightning/invoice", auth, require("./lightning/invoice")); + app.post("/lightning/query", auth, require("./lightning/query")); + app.post("/lightning/send", auth, require("./lightning/send")); + app.post("/lightning/user", auth, require("./lightning/user")); + + app.post("/bitcoin/send", auth, require("./bitcoin/send")); + app.post("/bitcoin/fee", auth, require("./bitcoin/fee")); + + app.post("/liquid/send", auth, require("./liquid/send")); + app.post("/liquid/fee", auth, require("./liquid/fee")); + + app.get("/payments", auth, async (req, res) => { + const payments = await db.Payment.findAll({ + where: { user_id: req.user.id }, + order: [["id", "DESC"]] + }); + + emit(req.user.username, "payments", payments); + res.send(payments); + }); +})(); diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 00000000..4d9127e1 --- /dev/null +++ b/routes/users.js @@ -0,0 +1,328 @@ +const axios = require("axios"); +const bcrypt = require("bcrypt"); +const jwt = require("jsonwebtoken"); +const uuidv4 = require("uuid/v4"); +const fb = "https://graph.facebook.com/"; +const authenticator = require("otplib").authenticator; + +const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); +let faucet = 1000; +const DAY = 24 * 60 * 60 * 1000; +require("../lib/whitelist"); + +const twofa = (req, res, next) => { + let { + user, + body: { token } + } = req; + if (!user.twofa || authenticator.check(token, user.otpsecret)) next(req, res); + else res.status(401).send("2fa required"); +}; + + +app.get("/liquidate", async (req, res) => { + let users = await db.User.findAll(); + for (let i = 0; i < users.length; i++) { + let user = users[i]; + if (!user.confidential) { + user.confidential = await lq.getNewAddress(); + user.liquid = (await lq.getAddressInfo( + user.confidential + )).unconfidential; + await user.save(); + } + + if (!user.otpsecret) { + user.otpsecret = authenticator.generateSecret(); + await user.save(); + } + } + res.end(); +}); + +const gift = async user => { + if (faucet > 0) { + user.balance = 100; + faucet -= 100; + const payment = await db.Payment.create({ + user_id: user.id, + hash: "Welcome Gift", + amount: 100, + currency: user.currency, + rate: app.get("rates")[user.currency], + received: true, + confirmed: 1, + asset: "GIFT" + }); + await user.save(); + } +}; + +app.post("/register", async (req, res) => { + let err = m => res.status(500).send(m); + let user = req.body; + if (!user.username) return err("Username required"); + + let exists = await db.User.count({ where: { username: user.username } }); + if (exists) return err("Username taken"); + + user.address = await bc.getNewAddress("", "bech32"); + user.confidential = await lq.getNewAddress(); + user.liquid = (await lq.getAddressInfo( + user.confidential + )).unconfidential; + user.name = user.username; + let ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + let countries = { + CA: "CAD", + US: "USD", + JP: "JPY", + CN: "CNY", + AU: "AUD", + GB: "GBP" + }; + + if (ip.startsWith('127') || ip.startsWith('192')) user.currency = "CAD"; + else { + let info = await axios.get(`http://api.ipstack.com/${ip}?access_key=${config.ipstack}`); + user.currency = countries[info.data.country_code] || "USD"; + } + + user.currencies = [user.currency]; + user.otpsecret = authenticator.generateSecret(); + + addresses[user.address] = user.username; + addresses[user.liquid] = user.username; + user = await db.User.create(user); + await gift(user); + + l.info("new user", user.username); + res.send(pick(user, ...whitelist)); +}); + +app.post("/disable2fa", auth, twofa, async (req, res) => { + let { user } = req; + user.twofa = false; + await user.save(); + emit(user.username, "user", user); + emit(user.username, "otpsecret", user.otpsecret); + l.info("disabled 2fa", user.username); + res.end(); +}); + +app.get("/otpsecret", auth, twofa, async (req, res) => { + let { user } = req; + emit(user.username, "otpsecret", user.otpsecret); + res.end(); +}); + +app.post("/2fa", auth, async (req, res) => { + let { user } = req; + try { + const isValid = authenticator.check(req.body.token, req.user.otpsecret); + if (isValid) { + user.twofa = true; + user.save(); + emit(user.username, "user", req.user); + } else { + return res.status(500).send("Invalid token"); + } + } catch (e) { + l.error("error setting up 2fa", e); + } + + l.info("enabled 2fa", user.username); + res.end(); +}); + +app.post("/user", auth, async (req, res) => { + try { + let { user } = req; + let { + username, + currency, + currencies, + twofa, + pin, + password, + passconfirm + } = req.body; + + let exists = await db.User.findOne({ + where: { username } + }); + + let token; + if (user.username !== username && exists) { + return res.status(500).send("Username taken"); + } else { + user.username = username; + addresses[user.address] = username; + addresses[user.liquid] = user.username; + token = jwt.sign({ username }, config.jwt); + res.cookie("token", token, { + expires: new Date(Date.now() + 432000000) + }); + } + + user.currencies = currencies; + user.currency = currency; + if (currencies.length) { + if (!currencies.includes(currency)) user.currency = currencies[0]; + } else { + user.currency = null; + } + + user.twofa = twofa; + user.pin = pin; + + if (password && password === passconfirm) { + user.password = await bcrypt.hash(password, 1); + } + + await user.save(); + emit(user.username, "user", req.user); + res.send({ user, token }); + } catch (e) { + l.error("error updating user", e); + } +}); + +app.post("/login", async (req, res) => { + let twofa = req.body.token; + + try { + let user = await db.User.findOne({ + where: { + username: req.body.username + } + }); + + if ( + !user || + (user.password && + !(await bcrypt.compare(req.body.password, user.password))) + ) { + l.warn("invalid username or password attempt"); + return res.status(401).end(); + } + + if (user.twofa && (typeof twofa === "undefined" || !authenticator.check(twofa, user.otpsecret))) { + return res.status(401).send("2fa required"); + } + + let payload = { username: user.username }; + let token = jwt.sign(payload, config.jwt); + res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); + + user = pick(user, ...whitelist); + res.send({ user, token }); + } catch (e) { + l.error("login error", e); + res.status(401).end(); + } +}); + +app.post("/facebookLogin", async (req, res) => { + let { accessToken, userID } = req.body; + let twofa = req.body.token; + + let url = `https://graph.facebook.com/debug_token?input_token=${accessToken}&access_token=${ + config.facebook.appToken + }`; + let check = await axios.get(url); + if (!check.data.data.is_valid) return res.status(401).end(); + + try { + let user = await db.User.findOne({ + where: { + username: req.body.userID + } + }); + + if (!user) { + user = await db.User.create(user); + user.username = userID; + user.name = (await axios.get( + `${fb}/me?access_token=${accessToken}` + )).data.name; + user.address = await bc.getNewAddress("", "bech32"); + user.confidential = await lq.getNewAddress(); + user.liquid = (await lq.getAddressInfo( + user.confidential + )).unconfidential; + user.password = await bcrypt.hash(accessToken, 1); + user.balance = 0; + user.pending = 0; + let friends = (await axios.get( + `${fb}/${userID}/friends?access_token=${accessToken}` + )).data.data; + if (friends.find(f => f.id === config.facebook.specialFriend)) { + user.friend = true; + user.limit = 200; + } + await user.save(); + addresses[user.address] = user.username; + addresses[user.liquid] = user.username; + await gift(user); + l.info("new facebook user", user.name); + } + + if (user.twofa && (typeof twofa === "undefined" || !authenticator.check(twofa, user.otpsecret))) + return res.status(401).send("2fa required"); + + user.pic = (await axios.get( + `${fb}/me/picture?access_token=${accessToken}&redirect=false` + )).data.data.url; + user.fbtoken = accessToken; + await user.save(); + + let payload = { username: user.username }; + let token = jwt.sign(payload, config.jwt); + res.cookie("token", token , { expires: new Date(Date.now() + 432000000) }); + res.send({ user, token }); + } catch (e) { + l.error("error during facebook login", e); + res.status(401).end(); + } +}); + +app.get("/me", async (req, res) => { + let { + data: { + data: { url } + } + } = await axios.get( + `${fb}/me/picture?access_token=EAAEIFqWk3ZAwBAEUfxQdH3T5CBKXmU8d7jQ5OTJBJZBiU1ZAp76lO26nh57WolM4R4JoKks9BCc49s8VrlEm2Ub1GlZCEzVD9fGxzUiranXDErDmR5gDUPKX3BhCsGA649a4hmbldRwKFTsmZCGZCergm9ACspKdTZB0WgFgA9wEdemIRIXuwCygNrymmKDh0Wd8nmoT4Hj3wZDZD&redirect=false` + ); + res.send(url); +}); + +app.get("/friends", auth, async (req, res) => { + try { + const { + data: { data } + } = await axios.get( + `${fb}/${req.user.username}/friends?access_token=${req.user.fbtoken}` + ); + + const friends = await Promise.all( + data.map(async f => { + f.pic = (await axios.get( + `${fb}/${f.id}/picture?redirect=false&type=small&access_token=${ + req.user.fbtoken + }` + )).data.data.url; + return f; + }) + ); + + res.send(friends); + } catch (e) { + res + .status(500) + .send("There was a problem getting your facebook friends: " + e); + } +}); + +setInterval(() => (faucet = 2000), DAY); |
