aboutsummaryrefslogtreecommitdiffstats
path: root/routes
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2020-03-11 15:10:40 -0700
committerAdam Soltys <[email protected]>2020-03-11 15:10:40 -0700
commitc0888fab024cc9f7c99812bfabf9d7774312dbf7 (patch)
treecad95683af97432f6092a2c2bae10fbb7a588db5 /routes
parentbf2b51f0c2ae9b8f0f330dde6411e45fc0da8a25 (diff)
downloadcoinos-server-c0888fab024cc9f7c99812bfabf9d7774312dbf7.tar.gz
coinos-server-c0888fab024cc9f7c99812bfabf9d7774312dbf7.zip
refactor
Diffstat (limited to 'routes')
-rw-r--r--routes/bitcoin/fee.js25
-rw-r--r--routes/bitcoin/receive.js143
-rw-r--r--routes/bitcoin/send.js93
-rw-r--r--routes/lightning/invoice.js27
-rw-r--r--routes/lightning/query.js7
-rw-r--r--routes/lightning/receive.js36
-rw-r--r--routes/lightning/send.js93
-rw-r--r--routes/lightning/user.js46
-rw-r--r--routes/liquid/fee.js25
-rw-r--r--routes/liquid/receive.js137
-rw-r--r--routes/liquid/send.js97
-rw-r--r--routes/payments.js40
-rw-r--r--routes/users.js328
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);