diff options
| author | Adam Soltys <[email protected]> | 2019-09-15 23:05:26 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2019-09-15 23:05:26 -0700 |
| commit | 765c6a42135b9334b616c26b22c816edd06e382a (patch) | |
| tree | 05da4f343358fb5e9a9083e348783f0fab973dd8 | |
| parent | 087fdd27f3683a1deb9376b7a72aa608e6fd8e6a (diff) | |
| download | coinos-server-765c6a42135b9334b616c26b22c816edd06e382a.tar.gz coinos-server-765c6a42135b9334b616c26b22c816edd06e382a.zip | |
refactor
| -rw-r--r-- | addInvoice.js | 25 | ||||
| -rw-r--r-- | addressBalance.js | 16 | ||||
| -rw-r--r-- | binance.js | 14 | ||||
| -rw-r--r-- | bitcoinPayments.js (renamed from zmq.js) | 0 | ||||
| -rw-r--r-- | db.js | 118 | ||||
| -rw-r--r-- | index.js | 745 | ||||
| -rw-r--r-- | kraken.js | 23 | ||||
| -rw-r--r-- | lightningPayments.js | 31 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | payUser.js | 38 | ||||
| -rw-r--r-- | payments.js | 19 | ||||
| -rw-r--r-- | sendCoins.js | 82 | ||||
| -rw-r--r-- | sendPayment.js | 87 | ||||
| -rw-r--r-- | stripe.js | 40 | ||||
| -rw-r--r-- | users.js | 308 | ||||
| -rw-r--r-- | yarn.lock | 12 |
16 files changed, 762 insertions, 797 deletions
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/zmq.js b/bitcoinPayments.js index 479298b8..479298b8 100644 --- a/zmq.js +++ b/bitcoinPayments.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 = {}; +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; + 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; + 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 = {}; + fields[f] = { + type: type, + field: f, + primaryKey: isKey, + autoIncrement: isKey + }; + }); - let options = {}; + db[k] = db.define(k, fields, { tableName: t }); - if (t === "users") { - options = { - before: (findOptions, args, context) => { - findOptions.where = { id: context.user.id }; - return findOptions; - } - }; - } + let typefields = {}; - if (t === "payments") { - db["Payment"].belongsTo(db["User"], { - as: "user", - foreignKey: "user_id" - }); + let options = {}; - options = { - before: (findOptions, args, context) => { - findOptions.where = { user_id: context.user.id }; - return findOptions; - } - }; - } + if (t === "users") { + options = { + before: (findOptions, args, context) => { + findOptions.where = { 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; @@ -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 <[email protected]>", - to: user.email, - subject: "CoinOS Email Verification", - html: `Visit <a href="https://coinos.io/verifyEmail/${user.username}/${user.emailToken}">https://coinos.io/verify/${user.username}/${user.emailToken}</a> 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 <[email protected]>", - to: user.email, - subject: "CoinOS Password Reset", - html: `Visit <a href="https://coinos.io/reset/${user.username}/${user.token}">https://coinos.io/reset/${user.username}/${user.token}</a> 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 <[email protected]>", + to: user.email, + subject: "CoinOS Email Verification", + html: `Visit <a href="https://coinos.io/verifyEmail/${user.username}/${ + user.emailToken + }">https://coinos.io/verify/${user.username}/${ + user.emailToken + }</a> 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 <[email protected]>", + to: user.email, + subject: "CoinOS Password Reset", + html: `Visit <a href="https://coinos.io/reset/${user.username}/${ + user.token + }">https://coinos.io/reset/${user.username}/${ + user.token + }</a> 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); + } + }); +}; @@ -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" |
