From 765c6a42135b9334b616c26b22c816edd06e382a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 15 Sep 2019 23:05:26 -0700 Subject: refactor --- addInvoice.js | 25 ++ addressBalance.js | 16 ++ binance.js | 14 + bitcoinPayments.js | 131 +++++++++ db.js | 132 +++++---- index.js | 745 +-------------------------------------------------- kraken.js | 23 ++ lightningPayments.js | 31 +++ package.json | 1 + payUser.js | 38 +++ payments.js | 19 ++ sendCoins.js | 82 ++++++ sendPayment.js | 87 ++++++ stripe.js | 40 +++ users.js | 308 +++++++++++++++++++++ yarn.lock | 12 + zmq.js | 131 --------- 17 files changed, 900 insertions(+), 935 deletions(-) create mode 100644 addInvoice.js create mode 100644 addressBalance.js create mode 100644 binance.js create mode 100644 bitcoinPayments.js create mode 100644 kraken.js create mode 100644 lightningPayments.js create mode 100644 payUser.js create mode 100644 payments.js create mode 100644 sendCoins.js create mode 100644 sendPayment.js create mode 100644 stripe.js create mode 100644 users.js delete mode 100644 zmq.js diff --git a/addInvoice.js b/addInvoice.js new file mode 100644 index 00000000..eca884dd --- /dev/null +++ b/addInvoice.js @@ -0,0 +1,25 @@ +module.exports = (app, db, lnb) => 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").ask, + tip + }); + + res.send(invoice); +}; diff --git a/addressBalance.js b/addressBalance.js new file mode 100644 index 00000000..b88e758d --- /dev/null +++ b/addressBalance.js @@ -0,0 +1,16 @@ +const config = require("./config"); + +module.exports = async (req, res) => { + let network = config.bitcoin.network === "mainnet" ? "main" : "test3"; + let { address } = req.params; + + try { + res.send( + (await axios.get( + `https://api.blockcypher.com/v1/btc/${network}/addrs/${address}/balance` + )).data + ); + } catch (e) { + res.status(500).send("Problem getting address balance"); + } +}; diff --git a/binance.js b/binance.js new file mode 100644 index 00000000..8edcc5da --- /dev/null +++ b/binance.js @@ -0,0 +1,14 @@ +const WebSocket = require("ws"); + +module.exports = app => { + const binance = new WebSocket( + "wss://stream.binance.com:9443/ws/btcusdt@ticker" + ); + + binance.onmessage = async function(event) { + let msg = JSON.parse(event.data); + app.set("bid", msg.b); + app.set("ask", msg.a); + app.set("last", msg.l); + }; +}; diff --git a/bitcoinPayments.js b/bitcoinPayments.js new file mode 100644 index 00000000..479298b8 --- /dev/null +++ b/bitcoinPayments.js @@ -0,0 +1,131 @@ +const reverse = require("buffer-reverse"); +const zmq = require("zeromq"); +const config = require("./config"); +const Sequelize = require("sequelize"); + +const bitcoin = require("bitcoinjs-lib"); +const l = console.log; + +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"); + +module.exports = (app, bc, db, addresses, payments, emit) => { + zmqRawTx.on("message", (topic, message, sequence) => { + message = 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 => { + let network = config.bitcoin.network; + if (network === "mainnet") { + network = "bitcoin"; + } + + let address; + try { + address = bitcoin.address.fromOutputScript( + o.script, + bitcoin.networks[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: { + hash: address, + received: null, + amount: { + [Sequelize.Op.gt]: 0 + } + }, + order: [["createdAt", "DESC"]] + }); + + let tip = null; + if (invoices.length) tip = invoices[0].tip; + + let confirmed = false; + + if (user.friend) { + user.balance += o.value; + confirmed = true; + } else { + user.pending += o.value; + } + + await user.save(); + emit(user.username, "user", user); + + await db.Payment.create({ + user_id: user.id, + hash, + amount: o.value, + currency: "CAD", + rate: app.get("rates").ask, + received: true, + tip, + confirmed + }); + + l("tx", message); + emit(user.username, "tx", message); + } + }) + ); + }); + + zmqRawBlock.on("message", async (topic, message, sequence) => { + topic = topic.toString("utf8"); + message = message.toString("hex"); + + switch (topic) { + case "rawblock": { + let block = bitcoin.Block.fromHex(message); + l("block", block.getHash().toString("hex")); + + const processPayment = async p => { + if ((await bc.getRawTransaction(p.hash, true)).confirmations > 0) { + let user = await db.User.findByPk(p.user_id); + l(user.pending); + p.confirmed = true; + user.balance += p.amount; + user.pending -= p.amount; + await user.save(); + await p.save(); + emit(user.username, "user", user); + emit(user.username, "block", message); + } + }; + + await db.Payment.findAll({ + where: { confirmed: false } + }).reduce(async (prev, payment) => { + const result = await prev; + return [...result, await processPayment(payment)]; + }, Promise.resolve([])); + + break; + } + } + }); +}; diff --git a/db.js b/db.js index a6c7c2d7..b2372c89 100644 --- a/db.js +++ b/db.js @@ -42,81 +42,79 @@ const db = new Sequelize(conf.database, conf.username, conf.password, { const gqlfields = {}; const gqltypes = {}; -const p = lnrpc => { - return new Promise((resolve, reject) => { - return auto.run(() => { - Object.keys(tables).forEach(k => { - let t = tables[k]; - let fields = {}; - - Object.keys(auto.tables[t]).forEach(f => { - let isKey = f === "id"; - let rawtype = auto.tables[t][f].type.toLowerCase(); - let type = Sequelize.STRING; - - if (rawtype.match(/^int/)) type = Sequelize.INTEGER; - if (rawtype.match(/^date/)) type = Sequelize.DATE; - - fields[f] = { - type: type, - field: f, - primaryKey: isKey, - autoIncrement: isKey - }; - }); +const p = new Promise((resolve, reject) => { + return auto.run(() => { + Object.keys(tables).forEach(k => { + let t = tables[k]; + let fields = {}; + + Object.keys(auto.tables[t]).forEach(f => { + let isKey = f === "id"; + let rawtype = auto.tables[t][f].type.toLowerCase(); + let type = Sequelize.STRING; + + if (rawtype.match(/^int/)) type = Sequelize.INTEGER; + if (rawtype.match(/^date/)) type = Sequelize.DATE; + + fields[f] = { + type: type, + field: f, + primaryKey: isKey, + autoIncrement: isKey + }; + }); + + db[k] = db.define(k, fields, { tableName: t }); + + let typefields = {}; + + let options = {}; + + if (t === "users") { + options = { + before: (findOptions, args, context) => { + findOptions.where = { id: context.user.id }; + return findOptions; + } + }; + } - db[k] = db.define(k, fields, { tableName: t }); - - let typefields = {}; - - let options = {}; - - if (t === "users") { - options = { - before: (findOptions, args, context) => { - findOptions.where = { id: context.user.id }; - return findOptions; - } - }; - } - - if (t === "payments") { - db["Payment"].belongsTo(db["User"], { - as: "user", - foreignKey: "user_id" - }); - - options = { - before: (findOptions, args, context) => { - findOptions.where = { user_id: context.user.id }; - return findOptions; - } - }; - } - - gqltypes[t] = new GraphQLObjectType({ - name: t, - desc: t, - fields: Object.assign(typefields, attributeFields(db[k])) + if (t === "payments") { + db["Payment"].belongsTo(db["User"], { + as: "user", + foreignKey: "user_id" }); - gqlfields[t] = { - type: new GraphQLList(gqltypes[t]), - args: Object.assign({}, defaultArgs(db[k]), defaultListArgs()), - resolve: resolver(db[k], options) + options = { + before: (findOptions, args, context) => { + findOptions.where = { user_id: context.user.id }; + return findOptions; + } }; - }); + } - db.gqlschema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: "RootQueryType", - fields: gqlfields - }) + gqltypes[t] = new GraphQLObjectType({ + name: t, + desc: t, + fields: Object.assign(typefields, attributeFields(db[k])) }); - resolve(db); + gqlfields[t] = { + type: new GraphQLList(gqltypes[t]), + args: Object.assign({}, defaultArgs(db[k]), defaultListArgs()), + resolve: resolver(db[k], options) + }; + }); + + db.gqlschema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: "RootQueryType", + fields: gqlfields + }) }); + + resolve(db); }); -}; +}); module.exports = p; diff --git a/index.js b/index.js index 4983a7ad..4878845f 100644 --- a/index.js +++ b/index.js @@ -1,39 +1,19 @@ -const { Client } = require("authy-client"); const axios = require("axios"); -const bcrypt = require("bcrypt"); const bodyParser = require("body-parser"); -const bolt11 = require("bolt11"); const compression = require("compression"); const cookieParser = require("cookie-parser"); const BitcoinCore = require("bitcoin-core"); const cors = require("cors"); const express = require("express"); -const fb = require("fb"); const graphqlHTTP = require("express-graphql"); -const jwt = require("jsonwebtoken"); const morgan = require("morgan"); -const mailgun = require("mailgun-js"); -const uuidv4 = require("uuid/v4"); -const reverse = require("buffer-reverse"); -const Sequelize = require("sequelize"); -const authyVerify = require("./authy"); const config = require("./config"); -const whitelist = require("./whitelist"); -const createLnrpc = require("./lib/createLnrpc"); -const bitcoin = require("bitcoinjs-lib"); const l = console.log; -/* eslint-disable-next-line */ -const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); -const authy = new Client({ key: config.authy.key }); - (async () => { - const lna = await createLnrpc(config.lna); - const lnb = await createLnrpc(config.lnb); const bc = new BitcoinCore(config.bitcoin); - const app = express(); app.enable("trust proxy"); app.use(bodyParser.urlencoded({ extended: true })); @@ -44,11 +24,10 @@ const authy = new Client({ key: config.authy.key }); app.use(morgan("combined")); const server = require("http").Server(app); - const db = await require("./db")(lna); + const db = await require("./db"); const [socket, emit] = require("./sockets")(app, db, server); const passport = require("./passport")(db); const auth = passport.authenticate("jwt", { session: false }); - const seen = []; app.use(passport.initialize()); @@ -61,36 +40,10 @@ const authy = new Client({ key: config.authy.key }); }) ); - 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; - - payment.received = true; - payment.user.balance += parseInt(msg.value); - payment.rate = app.get("rates").ask; - - await payment.save(); - await payment.user.save(); - payments.push(msg.payment_request); - - emit(payment.user.username, "invoice", msg); - emit(payment.user.username, "user", payment.user); - }; - - const invoices = lna.subscribeInvoices({}); - invoices.on("data", handlePayment); - - const invoicesb = lnb.subscribeInvoices({}); - invoicesb.on("data", handlePayment); + require("./binance")(app); + require("./kraken")(app, socket); + const seen = []; const addresses = {}; await db.User.findAll({ attributes: ["username", "address"] @@ -102,697 +55,15 @@ const authy = new Client({ key: config.authy.key }); attributes: ["hash"] })).map(p => p.hash); - require("./zmq")(app, bc, db, addresses, payments, emit); - - 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"); - if (user.password.length < 2) return err("Password too short"); - - let exists = await db.User.count({ where: { username: user.username } }); - if (exists) return err("Username taken"); - - user.address = (await lna.newAddress({ type: 1 }, lna.meta)).address; - user.password = await bcrypt.hash(user.password, 1); - user.name = user.username; - addresses[user.address] = user.username; - - await db.User.create(user); - res.send(pick(user, ...whitelist)); - }); - - const requestEmail = async user => { - user.emailToken = uuidv4(); - await user.save(); - - let mg = mailgun(config.mailgun); - let msg = { - from: "CoinOS ", - to: user.email, - subject: "CoinOS Email Verification", - html: `Visit https://coinos.io/verify/${user.username}/${user.emailToken} to verify your email address.` - }; - - try { - mg.messages().send(msg); - } catch (e) { - l(e); - } - }; - - const requestPhone = async user => { - user.phoneToken = Math.floor(100000 + Math.random() * 900000); - await user.save(); - const client = require("twilio")( - config.twilio.sid, - config.twilio.authToken - ); - - await client.messages.create({ - body: user.phoneToken, - from: config.twilio.number, - to: user.phone - }); - }; - - app.post("/requestEmail", auth, async (req, res) => { - req.user.email = req.body.email; - await requestEmail(req.user); - res.end(); - }); - - app.post("/requestPhone", auth, async (req, res) => { - req.user.phone = req.body.phone; - await requestPhone(req.user); - res.end(); - }); - - app.post("/user", auth, async (req, res) => { - let { user } = req; - let { - email, - phone, - twofa, - pin, - pinconfirm, - password, - passconfirm - } = req.body; - - if (user.email !== email && require("email-validator").validate(email)) { - user.email = email; - user.emailVerified = false; - requestEmail(user); - } - - if (user.phone !== phone) { - user.phone = phone; - user.phoneVerified = false; - requestPhone(user); - } - - user.email = email; - user.phone = phone; - user.twofa = twofa; - - if (password && password === passconfirm) { - user.password = await bcrypt.hash(password, 1); - } - - if (pin && pin === pinconfirm) user.pin = await bcrypt.hash(pin, 1); - - if (twofa && !user.authyId && user.phoneVerified) { - try { - let r = await authy.registerUser({ countryCode: "CA", email, phone }); - user.authyId = r.user.id; - } catch (e) { - l(e); - } - } - - await user.save(); - emit(req.user.username, "user", req.user); - res.send(user); - }); - - app.post("/forgot", async (req, res) => { - let { user } = req; - let mg = mailgun(config.mailgun); - let msg = { - from: "CoinOS ", - to: user.email, - subject: "CoinOS Password Reset", - html: `Visit https://coinos.io/reset/${user.username}/${user.token} to reset your password.` - }; - - try { - mg.messages().send(msg); - } catch (e) { - l(e); - } - }); - - app.get("/verifyEmail/:username/:token", auth, async (req, res) => { - let user = await db.User.findOne({ - where: { - username: req.params.username, - emailToken: req.params.token - } - }); - - if (user) { - user.emailVerified = true; - await user.save(); - - emit(user.username, "user", user); - emit(user.username, "emailVerified", true); - - res.end(); - } else { - res.status(500).send("invalid token or username"); - } - }); - - app.get("/verifyPhone/:username/:token", auth, async (req, res) => { - let user = await db.User.findOne({ - where: { - username: req.params.username, - phoneToken: req.params.token - } - }); - - console.log(user, req.params); - - if (user) { - user.phoneVerified = true; - await user.save(); - - emit(user.username, "user", user); - emit(user.username, "phoneVerified", true); - res.end(); - } else { - res.status(500).send("invalid token or username"); - } - }); - - app.post("/openchannel", auth, async (req, res) => { - let err = m => res.status(500).send(m); - if (req.user.balance < 10000) { - return err("Need at least 10000 satoshis for channel opening"); - } - - let pending = await lna.pendingChannels({}, lna.meta); - let busypeers = pending.pending_open_channels.map( - c => c.channel.remote_node_pub - ); - let peer = lna.channelpeers.find(p => !busypeers.includes(p)); - let sent = false; - - l("channeling"); - - if (!peer) { - res - .status(500) - .send("All peers have pending channel requests, try again later"); - return; - } - - try { - let amount = Math.min(req.user.balance, 16777216); - - let openchannel = async (peer, amount) => { - let channel = await lna.openChannel({ - node_pubkey: new Buffer(peer, "hex"), - local_funding_amount: amount - }); - - channel.on("data", async data => { - if (sent || !data.chan_pending) return; - req.user.balance += amount; - req.user.balance -= amount; - req.user.channel = reverse(data.chan_pending.txid).toString("hex"); - await req.user.save(); - emit(req.user.username, "user", req.user); - res.send(data); - sent = true; - }); - - channel.on("error", err => { - l("channel error", peer, err); - let msg = err.message; - - if ( - msg.startsWith("Multiple") || - msg.startsWith("You gave") || - msg.startsWith("peer") - ) { - busypeers.push(peer); - peer = lna.channelpeers.find(p => !busypeers.includes(p)); - if (peer) { - return openchannel(peer, amount); - } else { - return res - .status(500) - .send( - "All peers are busy, couldn't open a channel, wait a few blocks and try again" - ); - } - } - - if (msg.startsWith("not enough")) { - msg = "Server wallet is busy. Wait for a block and try again"; - } - - return res.status(500).send(msg); - }); - }; - - openchannel(peer, amount); - } catch (err) { - l("rpc error", peer, err); - return res.status(500).send(err); - } - }); - - app.get("/channels", auth, async (req, res) => { - return res.send(await lna.listChannels()); - }); - - app.get("/peers", auth, async (req, res) => { - return res.send(await lna.listPeers()); - }); - - app.post("/sendPayment", auth, async (req, res) => { - let hash = req.body.payreq; - let payreq = bolt11.decode(hash); - l("sending lightning", req.user.username, payreq.satoshis); - - if (seen.includes(hash)) { - 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: req.user.username - } - }, - { transaction } - ); - - if (balance < payreq.satoshis) { - throw new Error(); - } - - req.user.balance -= payreq.satoshis; - await req.user.save({ transaction }); - }); - } catch (e) { - return res.status(500).send("Not enough satoshis"); - } - - const stream = lna.sendPayment(lna.meta, {}); - stream.write({ payment_request: req.body.payreq }); - - stream.on("data", async m => { - if (m.payment_error) { - res.status(500).send(m.payment_error); - } else { - if (seen.includes(m.payment_preimage)) return; - seen.push(m.payment_preimage); - - let total = parseInt(m.payment_route.total_amt); - req.user.balance -= total - payreq.satoshis; - - await db.transaction(async transaction => { - await req.user.save({ transaction }); - - await db.Payment.create( - { - amount: -total, - user_id: req.user.id, - hash, - rate: app.get("rates").ask, - currency: "CAD" - }, - { transaction } - ); - }); - - emit(req.user.username, "user", req.user); - - if (payreq.payeeNodeKey === config.lnb.id) { - let invoice = await lna.addInvoice({ value: payreq.satoshis }); - 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(m); - } - }); - - stream.on("error", e => { - let msg = e.message; - - res.status(500).send(msg); - }); - }); - - app.get("/friends", auth, async (req, res) => { - try { - let friends = (await fb.api( - `/${req.user.username}/friends?access_token=${req.user.fbtoken}` - )).data; - friends = await Promise.all( - friends.map(async f => { - let pic = (await fb.api( - `/${f.id}/picture?redirect=false&type=small&access_token=${req.user.fbtoken}` - )).data; - f.pic = pic.url; - return f; - }) - ); - - res.send(friends); - } catch (e) { - res - .status(500) - .send("There was a problem getting your facebook friends: ", e); - } - }); - - app.post("/payUser", auth, 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, - currency: "CAD", - rate: app.get("rates").ask, - tip: 0 - }); - - req.url = "/sendPayment"; - req.body.payreq = invoice.payment_request; - return app._router.handle(req, res); - }); - - app.post("/sendCoins", auth, async (req, res) => { - const MINFEE = 3000; - - let { address, amount } = req.body; - - l("sending coins", req.user.username, amount, address); - - if (amount === req.user.balance) { - amount = req.user.balance - MINFEE; - } - - try { - await db.transaction(async transaction => { - let { balance } = await db.User.findOne( - { - where: { - username: req.user.username - } - }, - { transaction } - ); - - if (amount > balance) { - l("amount exceeds balance", amount, balance); - throw new Error("insufficient funds"); - } - - req.user.balance -= parseInt(amount) + 10000; - await req.user.save({ transaction }); - }); - } catch (e) { - return res.status(500).send("Not enough satoshis"); - } - - try { - if (config.bitcoin.walletpass) - await bc.walletPassphrase(config.bitcoin.walletpass, 300); - - let txid = await bc.sendToAddress( - address, - (amount / 100000000).toFixed(8) - ); - - let txhex = await bc.getRawTransaction(txid); - let tx = bitcoin.Transaction.fromHex(txhex); - - let inputTotal = await tx.ins.reduce(async (a, input) => { - let h = await bc.getRawTransaction(reverse(input.hash).toString("hex")); - return a + bitcoin.Transaction.fromHex(h).outs[input.index].value; - }, 0); - let outputTotal = tx.outs.reduce((a, b) => a + b.value, 0); - - let fees = inputTotal - outputTotal; - let total = Math.min(parseInt(amount) + fees, req.user.balance); - req.user.balance += 10000 - fees; - if (req.user.balance < 0) req.user.balance = 0; - - await db.transaction(async transaction => { - await req.user.save({ transaction }); - emit(req.user.username, "user", req.user); - - await db.Payment.create( - { - amount: -total, - user_id: req.user.id, - hash: txid, - rate: app.get("rates").ask, - currency: "CAD" - }, - { transaction } - ); - }); - - res.send({ txid, tx, amount, fees }); - } catch (e) { - l(e); - res.status(500).send(e.message); - } - }); - - app.post("/addInvoice", auth, 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").ask, - tip - }); - - res.send(invoice); - }); - - let fetchRates; - (fetchRates = async () => { - try { - let res = await axios.get( - "https://api.kraken.com/0/public/Ticker?pair=XBTCAD" - ); - let ask = res.data.result.XXBTZCAD.c[0]; - let now = new Date(); - let ts = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; - l(ts, "ask price:", ask); - app.set("rates", { ask }); - socket.emit("rate", ask); - } catch (e) { - l(e); - } - - setTimeout(fetchRates, 3000); - })(); - app.get("/rates", (req, res) => { res.send(app.get("rates")); }); - app.post("/login", async (req, res) => { - try { - let user = await db.User.findOne({ - where: { - username: req.body.username - } - }); - - if ( - !user || - !(await bcrypt.compare(req.body.password, user.password)) || - (user.twofa && !(await authyVerify(user))) - ) { - return res.status(401).end(); - } - - 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 (err) { - res.status(401).end(); - } - }); - - app.post("/facebookLogin", async (req, res) => { - let { accessToken, userID } = req.body; - - 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 fb.api(`/me?access_token=${accessToken}`)).name; - user.address = (await lna.newAddress({ type: 1 }, lna.meta)).address; - user.password = await bcrypt.hash(accessToken, 1); - user.balance = 0; - user.pending = 0; - let friends = (await fb.api( - `/${userID}/friends?access_token=${accessToken}` - )).data; - if (friends.find(f => f.id === config.facebook.specialFriend)) { - user.friend = true; - user.limit = 200; - } - await user.save(); - addresses[user.address] = user.username; - } - - user.pic = (await fb.api( - `/me/picture?access_token=${accessToken}&redirect=false` - )).data.url; - user.fbtoken = accessToken; - await user.save(); - - if (user.twofa && !(await authyVerify(user))) res.status(401).end(); - - 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 (err) { - l(err); - res.status(401).end(); - } - }); - - app.post("/buy", auth, async (req, res) => { - const stripe = require("stripe")(config.stripe); - const { token, amount, sat } = req.body; - let dollarAmount = parseInt(amount / 100); - - if (dollarAmount > req.user.limit) return res.status(500).end(); - - try { - const charge = await stripe.charges.create({ - amount, - currency: "cad", - description: "Bitcoin", - source: token - }); - - req.user.balance += parseInt(sat); - req.user.limit -= dollarAmount; - await req.user.save(); - emit(req.user.username, "user", req.user); - - await db.Payment.create({ - user_id: req.user.id, - hash: charge.balance_transaction, - amount: parseInt(sat), - currency: "CAD", - rate: app.get("rates").ask, - received: true, - tip: 0 - }); - - res.send(`Bought ${amount}`); - } catch (e) { - console.log(e); - res.status(500).send(e); - } - }); - - app.post("/order", auth, async (req, res) => { - await db.Order.create({ - amount: 1, - price: 2, - type: "BUY", - pair: "BTC/CAD" - }); - }); - - app.get("/me", async (req, res) => { - let data = (await fb.api( - "/me/picture?access_token=EAAEIFqWk3ZAwBAEUfxQdH3T5CBKXmU8d7jQ5OTJBJZBiU1ZAp76lO26nh57WolM4R4JoKks9BCc49s8VrlEm2Ub1GlZCEzVD9fGxzUiranXDErDmR5gDUPKX3BhCsGA649a4hmbldRwKFTsmZCGZCergm9ACspKdTZB0WgFgA9wEdemIRIXuwCygNrymmKDh0Wd8nmoT4Hj3wZDZD&redirect=false" - )).data; - res.send(data.url); - }); - - app.get("/exchanges", async (req, res) => { - let r = await axios.get( - "https://min-api.cryptocompare.com/data/v2/all/exchanges?fsym=BTC&api_key=6dc6d605ac45dcdbbc44d67111a2f03ba42ca8f54c5d4bf21069d2d3e99a89f0" - ); - res.send(r.data); - }); + require("./payments")(app, auth, addresses, bc, db, emit, seen, payments); + require("./users")(addresses, auth, app, bc, db, emit); + require("./stripe")(auth, app, db, emit); - app.get("/currencies", async (req, res) => { - res.send(["CAD", "USD"]); - }); - - app.get("/balance/:address", async (req, res) => { - let network = config.bitcoin.network === "mainnet" ? "main" : "test3"; - let { address } = req.params; - - try { - res.send( - (await axios.get( - `https://api.blockcypher.com/v1/btc/${network}/addrs/${address}/balance` - )).data - ); - } catch (e) { - res.status(500).send("Problem getting address balance"); - } - }); + app.get("/balance/:address", require("./addressBalance")); app.use(function(err, req, res, next) { res.status(500); diff --git a/kraken.js b/kraken.js new file mode 100644 index 00000000..70d7d183 --- /dev/null +++ b/kraken.js @@ -0,0 +1,23 @@ +const axios = require("axios"); +const l = console.log; + +module.exports = (app, socket) => { + let fetchRates; + (fetchRates = async () => { + try { + let res = await axios.get( + "https://api.kraken.com/0/public/Ticker?pair=XBTCAD" + ); + let ask = res.data.result.XXBTZCAD.c[0]; + let now = new Date(); + let ts = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; + // l(ts, "kraken ask:", ask); + app.set("rates", { ask }); + socket.emit("rate", ask); + } catch (e) { + l(e); + } + + setTimeout(fetchRates, 3000); + })(); +}; diff --git a/lightningPayments.js b/lightningPayments.js new file mode 100644 index 00000000..ed1d3647 --- /dev/null +++ b/lightningPayments.js @@ -0,0 +1,31 @@ +module.exports = (app, db, lna, lnb, emit, payments) => { + 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; + + payment.received = true; + payment.user.balance += parseInt(msg.value); + payment.rate = app.get("rates").ask; + + await payment.save(); + await payment.user.save(); + payments.push(msg.payment_request); + + emit(payment.user.username, "invoice", msg); + emit(payment.user.username, "user", payment.user); + }; + + const invoices = lna.subscribeInvoices({}); + invoices.on("data", handlePayment); + + const invoicesb = lnb.subscribeInvoices({}); + invoicesb.on("data", handlePayment); +}; diff --git a/package.json b/package.json index b0e18e4c..6ab9823b 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "stripe": "^7.2.0", "twilio": "^3.32.0", "uuid": "^3.3.2", + "ws": "^7.1.2", "zeromq": "^5.1.0" }, "devDependencies": { diff --git a/payUser.js b/payUser.js new file mode 100644 index 00000000..98cc1ac8 --- /dev/null +++ b/payUser.js @@ -0,0 +1,38 @@ +module.exports = db => 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, + currency: "CAD", + rate: app.get("rates").ask, + tip: 0 + }); + + req.url = "/sendPayment"; + req.body.payreq = invoice.payment_request; + return app._router.handle(req, res); + } diff --git a/payments.js b/payments.js new file mode 100644 index 00000000..d2e89b44 --- /dev/null +++ b/payments.js @@ -0,0 +1,19 @@ +const createLnrpc = require("./lib/createLnrpc"); +const config = require("./config"); + +module.exports = async (app, auth, addresses, bc, db, emit, seen, payments) => { + const lna = await createLnrpc(config.lna); + const lnb = await createLnrpc(config.lnb); + + require("./bitcoinPayments")(app, bc, db, addresses, payments, emit); + require("./lightningPayments")(app, db, lna, lnb, emit, payments); + + app.post( + "/sendPayment", + auth, + require("./sendPayment")(app, db, emit, seen, lna, lnb) + ); + app.post("/payUser", auth, require("./payUser")(db, lnb)); + app.post("/sendCoins", auth, require("./sendCoins")(app, bc, db, emit)); + app.post("/addInvoice", auth, require("./addInvoice")(app, db, lnb)); +}; diff --git a/sendCoins.js b/sendCoins.js new file mode 100644 index 00000000..31d12c26 --- /dev/null +++ b/sendCoins.js @@ -0,0 +1,82 @@ +const bitcoin = require("bitcoinjs-lib"); +const config = require("./config"); +const reverse = require("buffer-reverse"); + +const l = console.log; + +module.exports = (app, bc, db, emit) => async (req, res) => { + const MINFEE = 3000; + + let { address, amount } = req.body; + + l("sending coins", req.user.username, amount, address); + + if (amount === req.user.balance) { + amount = req.user.balance - MINFEE; + } + + try { + await db.transaction(async transaction => { + let { balance } = await db.User.findOne( + { + where: { + username: req.user.username + } + }, + { transaction } + ); + + if (amount > balance) { + l("amount exceeds balance", amount, balance); + throw new Error("insufficient funds"); + } + + req.user.balance -= parseInt(amount) + 10000; + await req.user.save({ transaction }); + }); + } catch (e) { + return res.status(500).send("Not enough satoshis"); + } + + try { + if (config.bitcoin.walletpass) + await bc.walletPassphrase(config.bitcoin.walletpass, 300); + + let txid = await bc.sendToAddress(address, (amount / 100000000).toFixed(8)); + + let txhex = await bc.getRawTransaction(txid); + let tx = bitcoin.Transaction.fromHex(txhex); + + let inputTotal = await tx.ins.reduce(async (a, input) => { + let h = await bc.getRawTransaction(reverse(input.hash).toString("hex")); + return a + bitcoin.Transaction.fromHex(h).outs[input.index].value; + }, 0); + let outputTotal = tx.outs.reduce((a, b) => a + b.value, 0); + + let fees = inputTotal - outputTotal; + let total = Math.min(parseInt(amount) + fees, req.user.balance); + req.user.balance += 10000 - fees; + if (req.user.balance < 0) req.user.balance = 0; + + await db.transaction(async transaction => { + await req.user.save({ transaction }); + emit(req.user.username, "user", req.user); + + await db.Payment.create( + { + amount: -total, + user_id: req.user.id, + hash: txid, + rate: app.get("rates").ask, + currency: "CAD" + }, + { transaction } + ); + }); + + res.send({ txid, tx, amount, fees }); + } catch (e) { + l(e); + res.status(500).send(e.message); + } +}; diff --git a/sendPayment.js b/sendPayment.js new file mode 100644 index 00000000..d4391087 --- /dev/null +++ b/sendPayment.js @@ -0,0 +1,87 @@ +const bolt11 = require("bolt11"); +const config = require("./config"); + +const l = console.log; + +module.exports = (app, db, emit, seen, lna, lnb) => async (req, res) => { + let hash = req.body.payreq; + let payreq = bolt11.decode(hash); + l("sending lightning", req.user.username, payreq.satoshis); + + if (seen.includes(hash)) { + 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: req.user.username + } + }, + { transaction } + ); + + if (balance < payreq.satoshis) { + throw new Error(); + } + + req.user.balance -= payreq.satoshis; + await req.user.save({ transaction }); + }); + } catch (e) { + return res.status(500).send("Not enough satoshis"); + } + + const stream = lna.sendPayment(lna.meta, {}); + stream.write({ payment_request: req.body.payreq }); + + stream.on("data", async m => { + if (m.payment_error) { + res.status(500).send(m.payment_error); + } else { + if (seen.includes(m.payment_preimage)) return; + seen.push(m.payment_preimage); + + let total = parseInt(m.payment_route.total_amt); + req.user.balance -= total - payreq.satoshis; + + await db.transaction(async transaction => { + await req.user.save({ transaction }); + + await db.Payment.create( + { + amount: -total, + user_id: req.user.id, + hash, + rate: app.get("rates").ask, + currency: "CAD" + }, + { transaction } + ); + }); + + emit(req.user.username, "user", req.user); + + if (payreq.payeeNodeKey === config.lnb.id) { + let invoice = await lna.addInvoice({ value: payreq.satoshis }); + 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(m); + } + }); + + stream.on("error", e => { + let msg = e.message; + + res.status(500).send(msg); + }); +}; diff --git a/stripe.js b/stripe.js new file mode 100644 index 00000000..ba1f68f7 --- /dev/null +++ b/stripe.js @@ -0,0 +1,40 @@ +const config = require("./config"); + +module.exports = (auth, app, db, emit) => { + app.post("/buy", auth, async (req, res) => { + const stripe = require("stripe")(config.stripe); + const { token, amount, sat } = req.body; + let dollarAmount = parseInt(amount / 100); + + if (dollarAmount > req.user.limit) return res.status(500).end(); + + try { + const charge = await stripe.charges.create({ + amount, + currency: "cad", + description: "Bitcoin", + source: token + }); + + req.user.balance += parseInt(sat); + req.user.limit -= dollarAmount; + await req.user.save(); + emit(req.user.username, "user", req.user); + + await db.Payment.create({ + user_id: req.user.id, + hash: charge.balance_transaction, + amount: parseInt(sat), + currency: "CAD", + rate: app.get("rates").ask, + received: true, + tip: 0 + }); + + res.send(`Bought ${amount}`); + } catch (e) { + console.log(e); + res.status(500).send(e); + } + }); +}; diff --git a/users.js b/users.js new file mode 100644 index 00000000..84b9ec2a --- /dev/null +++ b/users.js @@ -0,0 +1,308 @@ +const { Client } = require("authy-client"); +const axios = require("axios"); +const bcrypt = require("bcrypt"); +const fb = require("fb"); +const jwt = require("jsonwebtoken"); +const mailgun = require("mailgun-js"); +const uuidv4 = require("uuid/v4"); + +const authyVerify = require("./authy"); +const config = require("./config"); +const authy = new Client({ key: config.authy.key }); +const whitelist = require("./whitelist"); + +const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); + +module.exports = (addresses, auth, app, bc, db, emit) => { + 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"); + if (user.password.length < 2) return err("Password too short"); + + let exists = await db.User.count({ where: { username: user.username } }); + if (exists) return err("Username taken"); + + user.address = await bc.getNewAddress("", "bech32"); + user.password = await bcrypt.hash(user.password, 1); + user.name = user.username; + addresses[user.address] = user.username; + + await db.User.create(user); + res.send(pick(user, ...whitelist)); + }); + + const requestEmail = async user => { + user.emailToken = uuidv4(); + await user.save(); + + let mg = mailgun(config.mailgun); + let msg = { + from: "CoinOS ", + to: user.email, + subject: "CoinOS Email Verification", + html: `Visit https://coinos.io/verify/${user.username}/${ + user.emailToken + } to verify your email address.` + }; + + try { + mg.messages().send(msg); + } catch (e) { + l(e); + } + }; + + const requestPhone = async user => { + user.phoneToken = Math.floor(100000 + Math.random() * 900000); + await user.save(); + const client = require("twilio")( + config.twilio.sid, + config.twilio.authToken + ); + + await client.messages.create({ + body: user.phoneToken, + from: config.twilio.number, + to: user.phone + }); + }; + + app.post("/requestEmail", auth, async (req, res) => { + req.user.email = req.body.email; + await requestEmail(req.user); + res.end(); + }); + + app.post("/requestPhone", auth, async (req, res) => { + req.user.phone = req.body.phone; + await requestPhone(req.user); + res.end(); + }); + + app.post("/user", auth, async (req, res) => { + let { user } = req; + let { + email, + phone, + twofa, + pin, + pinconfirm, + password, + passconfirm + } = req.body; + + if (user.email !== email && require("email-validator").validate(email)) { + user.email = email; + user.emailVerified = false; + requestEmail(user); + } + + if (user.phone !== phone) { + user.phone = phone; + user.phoneVerified = false; + requestPhone(user); + } + + user.email = email; + user.phone = phone; + user.twofa = twofa; + + if (password && password === passconfirm) { + user.password = await bcrypt.hash(password, 1); + } + + if (pin && pin === pinconfirm) user.pin = await bcrypt.hash(pin, 1); + + if (twofa && !user.authyId && user.phoneVerified) { + try { + let r = await authy.registerUser({ countryCode: "CA", email, phone }); + user.authyId = r.user.id; + } catch (e) { + l(e); + } + } + + await user.save(); + emit(req.user.username, "user", req.user); + res.send(user); + }); + + app.post("/forgot", async (req, res) => { + let { user } = req; + let mg = mailgun(config.mailgun); + let msg = { + from: "CoinOS ", + to: user.email, + subject: "CoinOS Password Reset", + html: `Visit https://coinos.io/reset/${user.username}/${ + user.token + } to reset your password.` + }; + + try { + mg.messages().send(msg); + } catch (e) { + l(e); + } + }); + + app.get("/verifyEmail/:username/:token", auth, async (req, res) => { + let user = await db.User.findOne({ + where: { + username: req.params.username, + emailToken: req.params.token + } + }); + + if (user) { + user.emailVerified = true; + await user.save(); + + emit(user.username, "user", user); + emit(user.username, "emailVerified", true); + + res.end(); + } else { + res.status(500).send("invalid token or username"); + } + }); + + app.get("/verifyPhone/:username/:token", auth, async (req, res) => { + let user = await db.User.findOne({ + where: { + username: req.params.username, + phoneToken: req.params.token + } + }); + + if (user) { + user.phoneVerified = true; + await user.save(); + + emit(user.username, "user", user); + emit(user.username, "phoneVerified", true); + res.end(); + } else { + res.status(500).send("invalid token or username"); + } + }); + + app.post("/login", async (req, res) => { + try { + let user = await db.User.findOne({ + where: { + username: req.body.username + } + }); + + if ( + !user || + !(await bcrypt.compare(req.body.password, user.password)) || + (user.twofa && !(await authyVerify(user))) + ) { + console.log("MeoWWW"); + return res.status(401).end(); + } + + 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 (err) { + console.log(err); + res.status(401).end(); + } + }); + + app.post("/facebookLogin", async (req, res) => { + let { accessToken, userID } = req.body; + + 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 fb.api(`/me?access_token=${accessToken}`)).name; + user.address = await bc.getNewAddress("", "bech32"); + user.password = await bcrypt.hash(accessToken, 1); + user.balance = 0; + user.pending = 0; + let friends = (await fb.api( + `/${userID}/friends?access_token=${accessToken}` + )).data; + if (friends.find(f => f.id === config.facebook.specialFriend)) { + user.friend = true; + user.limit = 200; + } + await user.save(); + addresses[user.address] = user.username; + } + + user.pic = (await fb.api( + `/me/picture?access_token=${accessToken}&redirect=false` + )).data.url; + user.fbtoken = accessToken; + await user.save(); + + if (user.twofa && !(await authyVerify(user))) res.status(401).end(); + + 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 (err) { + l(err); + res.status(401).end(); + } + }); + + app.get("/me", async (req, res) => { + let data = (await fb.api( + "/me/picture?access_token=EAAEIFqWk3ZAwBAEUfxQdH3T5CBKXmU8d7jQ5OTJBJZBiU1ZAp76lO26nh57WolM4R4JoKks9BCc49s8VrlEm2Ub1GlZCEzVD9fGxzUiranXDErDmR5gDUPKX3BhCsGA649a4hmbldRwKFTsmZCGZCergm9ACspKdTZB0WgFgA9wEdemIRIXuwCygNrymmKDh0Wd8nmoT4Hj3wZDZD&redirect=false" + )).data; + res.send(data.url); + }); + + app.get("/friends", auth, async (req, res) => { + try { + let friends = (await fb.api( + `/${req.user.username}/friends?access_token=${req.user.fbtoken}` + )).data; + friends = await Promise.all( + friends.map(async f => { + let pic = (await fb.api( + `/${f.id}/picture?redirect=false&type=small&access_token=${ + req.user.fbtoken + }` + )).data; + f.pic = pic.url; + return f; + }) + ); + + res.send(friends); + } catch (e) { + res + .status(500) + .send("There was a problem getting your facebook friends: ", e); + } + }); +}; diff --git a/yarn.lock b/yarn.lock index 09e63260..9d7dd45b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -810,6 +810,11 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== +async-limiter@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" @@ -8140,6 +8145,13 @@ ws@^6.0.0: dependencies: async-limiter "~1.0.0" +ws@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.1.2.tgz#c672d1629de8bb27a9699eb599be47aeeedd8f73" + integrity sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg== + dependencies: + async-limiter "^1.0.0" + ws@~6.1.0: version "6.1.4" resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" diff --git a/zmq.js b/zmq.js deleted file mode 100644 index 479298b8..00000000 --- a/zmq.js +++ /dev/null @@ -1,131 +0,0 @@ -const reverse = require("buffer-reverse"); -const zmq = require("zeromq"); -const config = require("./config"); -const Sequelize = require("sequelize"); - -const bitcoin = require("bitcoinjs-lib"); -const l = console.log; - -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"); - -module.exports = (app, bc, db, addresses, payments, emit) => { - zmqRawTx.on("message", (topic, message, sequence) => { - message = 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 => { - let network = config.bitcoin.network; - if (network === "mainnet") { - network = "bitcoin"; - } - - let address; - try { - address = bitcoin.address.fromOutputScript( - o.script, - bitcoin.networks[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: { - hash: address, - received: null, - amount: { - [Sequelize.Op.gt]: 0 - } - }, - order: [["createdAt", "DESC"]] - }); - - let tip = null; - if (invoices.length) tip = invoices[0].tip; - - let confirmed = false; - - if (user.friend) { - user.balance += o.value; - confirmed = true; - } else { - user.pending += o.value; - } - - await user.save(); - emit(user.username, "user", user); - - await db.Payment.create({ - user_id: user.id, - hash, - amount: o.value, - currency: "CAD", - rate: app.get("rates").ask, - received: true, - tip, - confirmed - }); - - l("tx", message); - emit(user.username, "tx", message); - } - }) - ); - }); - - zmqRawBlock.on("message", async (topic, message, sequence) => { - topic = topic.toString("utf8"); - message = message.toString("hex"); - - switch (topic) { - case "rawblock": { - let block = bitcoin.Block.fromHex(message); - l("block", block.getHash().toString("hex")); - - const processPayment = async p => { - if ((await bc.getRawTransaction(p.hash, true)).confirmations > 0) { - let user = await db.User.findByPk(p.user_id); - l(user.pending); - p.confirmed = true; - user.balance += p.amount; - user.pending -= p.amount; - await user.save(); - await p.save(); - emit(user.username, "user", user); - emit(user.username, "block", message); - } - }; - - await db.Payment.findAll({ - where: { confirmed: false } - }).reduce(async (prev, payment) => { - const result = await prev; - return [...result, await processPayment(payment)]; - }, Promise.resolve([])); - - break; - } - } - }); -}; -- cgit v1.2.3