# coinos-server Coinos is a bitcoin wallet app that supports payments over the bitcoin, liquid and lightning networks. Try it out at coinos.io. This repository contains the code for the backend API server which is implemented as a NodeJS application. The code for the frontend UI is tracked separately here. ## Install The follow commands will set up bitcoin, liquid and lightning nodes in regtest mode, along with a coinos app server, database, and front end UI. You'll need to have docker installed as a pre-requisite. ```bash git clone https://github.com/coinos/coinos-server cd coinos-server cp -r sampleconfig config cp -r sampledata data mkdir uploads sudo chown $(id -u):$(id -g) config/liquid sudo chown $(id -u):$(id -g) config/bitcoin cp .env.sample .env cp fx.sample fx docker network create net --gateway 172.18.0.1 --subnet 172.18.0.0/16 docker run -it -v $(pwd):/app --entrypoint pnpm asoltys/coinos-server i docker-compose up -d docker exec -i maria mysql -u root -ppassword < db/schema.sql docker exec -it bitcoin bitcoin-cli createwallet coinos docker exec -it bitcoin bitcoin-cli rescanblockchain docker exec -it bitcoin bitcoin-cli generatetoaddress 500 $(docker exec -it bitcoin bitcoin-cli getnewaddress "" "p2sh-segwit") docker exec -it liquid elements-cli createwallet coinos docker exec -it liquid elements-cli rescanblockchain docker exec -it liquid elements-cli sendtoaddress AzpsKhC6xE9FEK4aWAzMnbvueMLiSa5ym1xpuYogFkHzWgMHSt8B79aNNbFppQzCSQ2yZ9E4nL6RQJU7 1000000 docker exec -it liquid elements-cli generatetoaddress 1 AzpsKhC6xE9FEK4aWAzMnbvueMLiSa5ym1xpuYogFkHzWgMHSt8B79aNNbFppQzCSQ2yZ9E4nL6RQJU7 curl localhost:3119/register -H "content-type: application/json" -d '{"user": { "username": "coinosfees", "password": "password"}}' ``` After successful creation of all docker containers coinos ui will be available at http://localhost:8085 and coinos server will be available at http://localhost:3119 ### License This code is [licensed]. Coinos is free for personal use. Anyone can fork as long as it stays AGPLv3. To purchase a commercial license or to inquire about customized, managed instances - please reach out to us at [contact@coinos.io] [Config changes]:(#config-changes) [config/lnd/lnd.conf]:./sampleconfig/lnd/lnd.conf [pwd file]:./sampleconfig/lnd/pwd [licensed]:./LICENSE.md [contact@coinos.io]:mailto:contact@coinos.io [generate some BTC]:./doc/fund-server-bitcoin-cli.md e>
aboutsummaryrefslogtreecommitdiffstats
path: root/lib/sockets.js
blob: bd59ee3803fb9abc492066599440cda3ec53d01f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
const jwt = require("jsonwebtoken");
const uuidv4 = require("uuid/v4");
const WebSocket = require("ws");
const { differenceInSeconds, subSeconds } = require("date-fns");


wss = new WebSocket.Server({ clientTracking: false, noServer: true });

/* eslint-disable-next-line */
const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {});
const all = {};
const subscriptions = [];
const lastSeen = {};

emit = (username, type, data) => {
  if (data.username !== undefined) data = pick(data, ...whitelist);

  if (type === "payment" && data.amount > 0) {
    for (let i = subscriptions.length - 1; i >= 0; i--) {
      let s = subscriptions[i];
      if (
        s.invoice.amount === data.amount ||
        s.invoice.address === data.address ||
        s.invoice.unconfidential === data.address
      ) {
        s.ws.send(JSON.stringify({ type, data }));
        subscriptions.splice(i, 1);
      }
    }
  }

  if (!sockets[username]) return;
  for (id in sockets[username]) {
    const ws = sockets[username][id];
    ws.send(JSON.stringify({ type, data }));
  }
};

broadcast = (type, data) => {
  const sent = [];
  for (const username in sockets) {
    for (const id in sockets[username]) {
      const ws = sockets[username][id];
      ws.send(JSON.stringify({ type, data }));
      sent.push(ws.id);
    }
  }

  Object.values(all).map(ws => {
    if (!sent.includes(ws.id)) ws.send(JSON.stringify({ type, data }));
  });
};

server.on("upgrade", function(req, socket, head) {
  wss.handleUpgrade(req, socket, head, function(ws) {
    wss.emit("connection", ws, req);
  });
});

const names = {};

setInterval(() => {
  for (const username in sockets) {
    for (const id in sockets[username]) {
      let ws = sockets[username][id];
      ws.beats++;
      if (ws.beats > 10) ws.close();
    }
  }
}, 2000);

wss.on("connection", function(ws, req) {
  const id = uuidv4();
  ws.id = id;
  ws.beats = 0;
  ws.send(JSON.stringify({ type: "connected", data: id }));
  all[id] = ws;

  ws.on("message", async message => {
    let type, data;
    try {
      ({ type, data } = JSON.parse(message));
    } catch (e) {
      l.error("coudn't parse socket message");
    }

    switch (type) {
      case "heartbeat":
        ws.beats = 0;
        break;

      case "login":
        let token = data;

        if (differenceInSeconds(new Date(), lastSeen[token]) < 5) return;
        lastSeen[token] = new Date();

        try {
          if (!token || token === "null" || !jwt.decode(token)) return ws.close();
          const username = jwt.decode(token).username;
          if (!username) throw new Error("Invalid JWT token");

          if (!sockets[username]) sockets[username] = {};
          const existing = Object.keys(sockets[username]);
          if (existing.length > 4) {
            const p = existing.find(sid => sid !== id);
            sockets[username][p].send(JSON.stringify({ type: "logout" }));
            setTimeout(() => {
              if (sockets[username][p]) sockets[username][p].close();
            }, 5000);
          }
          sockets[username][id] = ws;
          names[id] = username;

          let user = await getUser(username);
          if (user.locked) throw new Error("Account is locked");
          let payments = await user.getPayments({
            where: {
              account_id: user.account_id
            },
            order: [["id", "DESC"]],
            limit: 12,
            include: {
              model: db.Account,
              as: "account"
            }
          });
          let accounts = await user.getAccounts();
          let keys = await user.getKeys();
          user = user.get({ plain: true });
          user.accounts = accounts;
          user.keys = keys;
          user.payments = payments;

          ws.user = user;
          ws.send(JSON.stringify({ type: "login", data: user }));
          ws.send(
            JSON.stringify({ type: "version", data: config.clientVersion })
          );
        } catch (e) {
          l.error("caught socket error", e.message);
          if (e.message.includes("locked")) {
            ws.send(JSON.stringify({ type: "locked" }));
            ws.send(JSON.stringify({ type: "logout" }));
          } 
          if (e.message.includes("not found"))
            ws.send(JSON.stringify({ type: "logout" }));
          setTimeout(() => ws && ws.close(), 1000);
        }
        break;
      case "token":
        if (!ws.user) return;
        ws.token = data;
        break;
      case "lnurl":
        sessions[data] = ws;
        break;
      case "updateMemo":
        const payment = await db.Payment.findOne({
          where: {
            user_id: ws.user.id,
            id: data.id
          },
          include: {
            model: db.Account,
            as: "account"
          }
        });
        if (payment) {
          payment.memo = data.memo;
          await payment.save();
          emit(ws.user.username, "payment", payment);
        }
        break;
      case "subscribe":
        subscriptions.push({ invoice: data, ws });
        break;
      default:
        l.warn("received socket message of unknown type", type, data);
    }
  });

  ws.on("close", function() {
    try {
      const username = names[id];
      if (sockets[username] && sockets[username][id]) {
        delete sockets[username][id];
      }

      if (all[id]) delete all[id];
    } catch (e) {
      l.error("problem closing socket", e.message);
    }
  });
});