import { Client } from "authy-client"; import axios from "axios"; import bcrypt from "bcrypt"; import bodyParser from "body-parser"; import bolt11 from "bolt11"; import compression from "compression"; import cookieParser from "cookie-parser"; import BitcoinCore from "bitcoin-core"; import cors from "cors"; import express from "express"; import fb from "fb"; import graphqlHTTP from "express-graphql"; import jwt from "jsonwebtoken"; import morgan from "morgan"; import mailgun from "mailgun-js"; import uuidv4 from "uuid/v4"; import reverse from "buffer-reverse"; import Sequelize from "sequelize"; import authyVerify from "./authy"; import config from "./config"; import whitelist from "./whitelist"; import createLnrpc from "./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 })); app.use(bodyParser.json()); app.use(cookieParser()); app.use(cors({ credentials: true, origin: "http://*:*" })); app.use(compression()); app.use(morgan("combined")); const server = require("http").Server(app); const db = await require("./db")(lna); const [socket, emit] = require("./sockets")(app, db, server); socket.listen(server); const passport = require("./passport")(db); const auth = passport.authenticate("jwt", { session: false }); const seen = []; app.use(passport.initialize()); app.use( "/graphql", auth, graphqlHTTP({ schema: db.gqlschema, graphiql: true }) ); 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); const addresses = {}; await db.User.findAll({ attributes: ["username", "address"] }).map(u => { addresses[u.address] = u.username; }); const payments = (await db.Payment.findAll({ attributes: ["hash"] })).map(p => p.hash); require("./zmq")(bc, db, addresses, payments); 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 { await bc.walletPassphrase(config.bitcoin.walletpass, 300); l("sending transaction"); let txid = await bc.sendToAddress( address, (amount / 100000000).toFixed(8) ); l("transaction sent", txid); 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); }); 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.use(function(err, req, res, next) { res.status(500); res.send("An error occurred"); l(err.stack); return res.end(); }); server.listen(config.port); })();