3 let Ws = module.exports;
5 let Cookies = require("../lib/cookies.js");
6 let request = require("./request.js");
8 let WSClient = require("ws");
11 * @param {Object} opts
12 * @param {String} opts.baseUrl
13 * @param {CookieStore} opts.cookieStore
14 * @param {Boolean} opts.debug
15 * @param {Function} opts.onClose
16 * @param {Function} opts.onError
17 * @param {Function} opts.onMessage
19 Ws.create = function ({
29 let defaultHeaders = {
31 //'Accept-Encoding': gzip, deflate, br
32 "Accept-Language": "en-US,en;q=0.9",
33 "Cache-Control": "no-cache",
34 Origin: "https://insight.dash.org",
35 referer: "https://insight.dash.org/insight/",
36 "sec-fetch-dest": "empty",
37 "sec-fetch-mode": "cors",
38 "sec-fetch-site": "same-origin",
45 let httpAgent = new Https.Agent({
51 // Get `sid` (session id) and ping/pong params
52 Eio3.connect = async function () {
54 let sidUrl = `${baseUrl}/socket.io/?EIO=3&transport=polling&t=${now}`;
56 let cookies = await cookieStore.get(sidUrl);
57 let sidResp = await request({
60 headers: Object.assign(
69 console.error(sidResp.toJSON());
70 throw new Error("bad response");
72 await cookieStore.set(sidUrl, sidResp);
74 // ex: `97:0{"sid":"xxxx",...}`
75 let msg = sidResp.body;
76 let colonIndex = msg.indexOf(":");
77 // 0 is CONNECT, which will always follow our first message
78 let start = colonIndex + ":0".length;
79 let len = parseInt(msg.slice(0, colonIndex), 10);
80 let json = msg.slice(start, start + (len - 1));
82 //console.log("Socket.io Connect:");
86 // @type {SocketIoHello}
87 let session = JSON.parse(json);
93 * @param {String} eventname
95 Eio3.subscribe = async function (sid, eventname) {
97 let subUrl = `${baseUrl}/socket.io/?EIO=3&transport=polling&t=${now}&sid=${sid}`;
98 let sub = JSON.stringify(["subscribe", eventname]);
99 // not really sure what this is, couldn't find documentation for it
100 let typ = 422; // 4 = MESSAGE, 2 = EVENT, 2 = ???
101 let msg = `${typ}${sub}`;
102 let len = msg.length;
103 let body = `${len}:${msg}`;
105 let cookies = await cookieStore.get(subUrl);
106 let subResp = await request({
110 headers: Object.assign(
112 "Content-Type": "text/plain;charset=UTF-8",
120 console.error(subResp.toJSON());
121 throw new Error("bad response");
123 await cookieStore.set(subUrl, subResp);
129 Eio3.poll = async function (sid) {
130 let now = Date.now();
131 let pollUrl = `${baseUrl}/socket.io/?EIO=3&transport=polling&t=${now}&sid=${sid}`;
133 let cookies = await cookieStore.get(pollUrl);
134 let pollResp = await request({
138 headers: Object.assign(
146 console.error(pollResp.toJSON());
147 throw new Error("bad response");
149 await cookieStore.set(pollUrl, pollResp);
151 return pollResp.body;
156 * @param {String} sid - session id (associated with AWS ALB cookie)
158 Eio3.connectWs = async function (sid) {
159 baseUrl = baseUrl.slice(4); // trim leading 'http'
161 `ws${baseUrl}/socket.io/?EIO=3&transport=websocket&sid=${sid}`.replace(
166 let cookies = await cookieStore.get(`${baseUrl}/`);
167 let ws = new WSClient(url, {
169 //perMessageDeflate: false,
170 //@ts-ignore - type info is wrong
171 headers: Object.assign(
179 let promise = new Promise(function (resolve) {
180 ws.on("open", function open() {
182 console.debug("=> Socket.io Hello ('2probe')");
187 ws.once("error", function (err) {
191 console.error("WebSocket Error:");
196 ws.once("message", function message(data) {
197 if ("3probe" === data.toString()) {
199 console.debug("<= Socket.io Welcome ('3probe')");
201 ws.send("5"); // no idea, but necessary
203 console.debug("=> Socket.io ACK? ('5')");
206 console.error("Unrecognized WebSocket Hello:");
207 console.error(data.toString());
215 return await promise;
218 /** @type import('ws')? */
221 wsc.init = async function () {
222 let session = await Eio3.connect();
224 console.debug("Socket.io Session:");
225 console.debug(session);
229 let sub = await Eio3.subscribe(session.sid, "inv");
231 console.debug("Socket.io Subscription:");
237 let poll = await Eio3.poll(session.sid);
239 console.debug("Socket.io Confirm:");
245 let ws = await Eio3.connectWs(session.sid);
249 ws.on("message", _onMessage);
250 ws.once("close", _onClose);
253 setTimeout(function () {
254 //ws.ping(); // standard
255 ws.send("2"); // socket.io
257 console.debug("=> Socket.io Ping");
259 }, session.pingInterval);
263 * @param {Buffer} buf
265 function _onMessage(buf) {
266 let msg = buf.toString();
267 if ("3" === msg.toString()) {
269 console.debug("<= Socket.io Pong");
276 if ("42" !== msg.slice(0, 2)) {
277 console.warn("Unknown message:");
282 /** @type {InsightPush} */
283 let [evname, data] = JSON.parse(msg.slice(2));
285 onMessage(evname, data);
295 // TODO put check function here
297 console.debug(`Received '${evname}':`);
304 function _onClose() {
306 console.debug("WebSocket Close");
314 wsc.close = function () {
322 * @param {String} baseUrl
323 * @param {Function} find
325 Ws.listen = async function (baseUrl, find) {
327 let p = new Promise(async function (resolve, reject) {
331 cookieStore: Cookies,
337 * @param {String} evname
338 * @param {InsightSocketEventData} data
340 async function (evname, data) {
343 result = await find(evname, data);
355 await ws.init().catch(reject);
357 let result = await p;
363 // TODO waitForVouts(baseUrl, [{ address, satoshis }])
366 * @param {String} baseUrl
367 * @param {String} addr
368 * @param {Number} [amount]
369 * @param {Number} [maxTxLockWait]
370 * @returns {Promise<SocketPayment>}
372 Ws.waitForVout = async function (
376 maxTxLockWait = 3000,
378 // Listen for Response
379 /** @type SocketPayment */
381 return await Ws.listen(baseUrl, findResponse);
384 * @param {String} evname
385 * @param {InsightSocketEventData} data
387 function findResponse(evname, data) {
388 if (!["tx", "txlock"].includes(evname)) {
392 let now = Date.now();
393 if (mempoolTx?.timestamp) {
394 // don't wait longer than 3s for a txlock
395 if (now - mempoolTx.timestamp > maxTxLockWait) {
401 // TODO should fetch tx and match hotwallet as vin
402 data.vout.some(function (vout) {
403 if (!(addr in vout)) {
407 let duffs = vout[addr];
408 if (amount && duffs !== amount) {
420 if ("txlock" !== evname) {
436 async function sleep(ms) {
437 return await new Promise(function (resolve) {
438 setTimeout(resolve, ms);