aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2019-09-15 23:05:26 -0700
committerAdam Soltys <[email protected]>2019-09-15 23:05:26 -0700
commit765c6a42135b9334b616c26b22c816edd06e382a (patch)
tree05da4f343358fb5e9a9083e348783f0fab973dd8
parent087fdd27f3683a1deb9376b7a72aa608e6fd8e6a (diff)
downloadcoinos-server-765c6a42135b9334b616c26b22c816edd06e382a.tar.gz
coinos-server-765c6a42135b9334b616c26b22c816edd06e382a.zip
refactor
-rw-r--r--addInvoice.js25
-rw-r--r--addressBalance.js16
-rw-r--r--binance.js14
-rw-r--r--bitcoinPayments.js (renamed from zmq.js)0
-rw-r--r--db.js118
-rw-r--r--index.js745
-rw-r--r--kraken.js23
-rw-r--r--lightningPayments.js31
-rw-r--r--package.json1
-rw-r--r--payUser.js38
-rw-r--r--payments.js19
-rw-r--r--sendCoins.js82
-rw-r--r--sendPayment.js87
-rw-r--r--stripe.js40
-rw-r--r--users.js308
-rw-r--r--yarn.lock12
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
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 = {};
+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;
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 <[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);
+ }
+ });
+};
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"