diff --git a/package-lock.json b/package-lock.json index 8db1b07..fe608b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,10 @@ "@rsksmart/rsk-utils": "^1.1.0", "axios": "^0.22.0", "dotenv": "^10.0.0", - "express": "^4.17.1" + "express": "^4.17.1", + "node-cache": "^5.1.2", + "socket.io": "^4.4.0", + "socket.io-client": "^2.4.0" }, "devDependencies": { "@types/dotenv": "^8.2.0", @@ -1369,6 +1372,11 @@ "@types/node": "*" } }, + "node_modules/@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -1378,6 +1386,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, "node_modules/@types/dotenv": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz", @@ -1475,8 +1493,7 @@ "node_modules/@types/node": { "version": "16.10.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz", - "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==", - "dev": true + "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==" }, "node_modules/@types/prettier": { "version": "2.4.2", @@ -1895,6 +1912,11 @@ "node": ">=0.4.0" } }, + "node_modules/after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2093,6 +2115,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -2233,12 +2260,33 @@ "@babel/core": "^7.0.0" } }, + "node_modules/backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2256,6 +2304,11 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "node_modules/bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", @@ -2577,6 +2630,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -2632,11 +2693,20 @@ "node": ">= 0.8" } }, + "node_modules/component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "node_modules/component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "node_modules/concat-map": { "version": "0.0.1", @@ -2708,6 +2778,18 @@ "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", "dev": true }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3015,6 +3097,152 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.0.tgz", + "integrity": "sha512-ErhZOVu2xweCjEfYcTdkCnEYUiZgkAcBBAhW4jbIvNG8SLU3orAqoJCiytZjYF7eTpVmmCrLDjLIEaPlUAs1uw==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-client": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", + "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "dependencies": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.4.2", + "xmlhttprequest-ssl": "~1.6.2", + "yeast": "0.1.2" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/engine.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "dependencies": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.4", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.2.tgz", + "integrity": "sha512-wuiO7qO/OEkPJSFueuATIXtrxF7/6GTbAO9QLv7nnbjwZ5tYhLm9zxvLwxstRs0dcT0KUlWTjtIOs1T86jt12g==", + "dependencies": { + "base64-arraybuffer": "~1.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser/node_modules/base64-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz", + "integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -4268,6 +4496,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "dependencies": { + "isarray": "2.0.1" + } + }, + "node_modules/has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4598,6 +4839,11 @@ "node": ">=0.8.19" } }, + "node_modules/indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4959,6 +5205,11 @@ "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", "dev": true }, + "node_modules/isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6262,6 +6513,17 @@ "node": ">= 0.6" } }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6381,6 +6643,14 @@ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", "dev": true }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", @@ -6583,6 +6853,16 @@ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, + "node_modules/parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "node_modules/parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7308,6 +7588,118 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/socket.io": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.0.tgz", + "integrity": "sha512-bnpJxswR9ov0Bw6ilhCvO38/1WPtE3eA2dtxi2Iq4/sFebiDJQzgKNYA7AuVVdGW09nrESXd90NbZqtDd9dzRQ==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==" + }, + "node_modules/socket.io-client": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", + "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", + "dependencies": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "engine.io-client": "~3.5.0", + "has-binary2": "~1.0.2", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/socket.io-client/node_modules/socket.io-parser": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", + "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", + "dependencies": { + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "isarray": "2.0.1" + } + }, + "node_modules/socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "dependencies": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7754,6 +8146,11 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "node_modules/to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -8484,6 +8881,14 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/xmlhttprequest-ssl": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", + "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -8526,6 +8931,11 @@ "node": ">=10" } }, + "node_modules/yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -9572,6 +9982,11 @@ "@types/node": "*" } }, + "@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -9581,6 +9996,16 @@ "@types/node": "*" } }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, "@types/dotenv": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz", @@ -9677,8 +10102,7 @@ "@types/node": { "version": "16.10.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz", - "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==", - "dev": true + "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==" }, "@types/prettier": { "version": "2.4.2", @@ -9962,6 +10386,11 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -10107,6 +10536,11 @@ "es-abstract": "^1.19.0" } }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -10219,12 +10653,27 @@ "babel-preset-current-node-syntax": "^1.0.0" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -10239,6 +10688,11 @@ "file-uri-to-path": "1.0.0" } }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", @@ -10482,6 +10936,11 @@ "wrap-ansi": "^7.0.0" } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -10527,11 +10986,20 @@ "delayed-stream": "~1.0.0" } }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "concat-map": { "version": "0.0.1", @@ -10591,6 +11059,15 @@ "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", "dev": true }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -10836,6 +11313,110 @@ "once": "^1.4.0" } }, + "engine.io": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.0.tgz", + "integrity": "sha512-ErhZOVu2xweCjEfYcTdkCnEYUiZgkAcBBAhW4jbIvNG8SLU3orAqoJCiytZjYF7eTpVmmCrLDjLIEaPlUAs1uw==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "requires": {} + } + } + }, + "engine.io-client": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", + "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "requires": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.4.2", + "xmlhttprequest-ssl": "~1.6.2", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "engine.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.4", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} + } + } + }, + "engine.io-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.2.tgz", + "integrity": "sha512-wuiO7qO/OEkPJSFueuATIXtrxF7/6GTbAO9QLv7nnbjwZ5tYhLm9zxvLwxstRs0dcT0KUlWTjtIOs1T86jt12g==", + "requires": { + "base64-arraybuffer": "~1.0.1" + }, + "dependencies": { + "base64-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz", + "integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==" + } + } + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -11752,6 +12333,19 @@ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "dev": true }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -11991,6 +12585,11 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -12238,6 +12837,11 @@ "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", "dev": true }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -13261,6 +13865,14 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "requires": { + "clone": "2.x" + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -13356,6 +13968,11 @@ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, "object-inspect": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", @@ -13506,6 +14123,16 @@ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -14043,6 +14670,102 @@ "is-fullwidth-code-point": "^3.0.0" } }, + "socket.io": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.0.tgz", + "integrity": "sha512-bnpJxswR9ov0Bw6ilhCvO38/1WPtE3eA2dtxi2Iq4/sFebiDJQzgKNYA7AuVVdGW09nrESXd90NbZqtDd9dzRQ==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==" + }, + "socket.io-client": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", + "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", + "requires": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "engine.io-client": "~3.5.0", + "has-binary2": "~1.0.2", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "socket.io-parser": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", + "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", + "requires": { + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "isarray": "2.0.1" + } + } + } + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -14389,6 +15112,11 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -14920,6 +15648,11 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmlhttprequest-ssl": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", + "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -14953,6 +15686,11 @@ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 782c974..2bab0a5 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "@rsksmart/rsk-utils": "^1.1.0", "axios": "^0.22.0", "dotenv": "^10.0.0", - "express": "^4.17.1" + "express": "^4.17.1", + "node-cache": "^5.1.2", + "socket.io": "^4.4.0" } } diff --git a/src/api/index.ts b/src/api/index.ts index a0ea1c8..95fd81c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,69 +1,85 @@ -import { Application, Request, Response } from 'express' +import { Application, NextFunction, Request, Response } from 'express' import { RSKExplorerAPI } from '../rskExplorerApi' -import { CoinMarketCapAPI } from '../coinmatketcap' +import { CoinMarketCapAPI } from '../coinmarketcap' import { registeredDapps as _registeredDapps } from '../registered_dapps' import { PricesQueryParams } from './types' -import { validatePricesRequest } from '../coinmatketcap/validations' +import { isConvertSupported, isTokenSupported } from '../coinmarketcap/validations' +import NodeCache from 'node-cache' +import { findInCache, storeInCache } from '../coinmarketcap/priceCache' +import { errorHandler } from '../middleware' const responseJsonOk = (res: Response) => res.status(200).json.bind(res) -const makeRequestFactory = (console) => async (req, res, query) => { - try { - console.log(req.url) - const result = await query() - res.status(200).json(result) - } catch (e: any) { - console.error(e) - res.status(500).send(e.message) - } -} - type APIOptions = { rskExplorerApi: RSKExplorerAPI coinMarketCapApi: CoinMarketCapAPI registeredDapps: typeof _registeredDapps + priceCache: NodeCache logger?: any + chainId: number } export const setupApi = (app: Application, { - rskExplorerApi, coinMarketCapApi, registeredDapps, logger = { log: () => {}, error: () => {} } + rskExplorerApi, coinMarketCapApi, registeredDapps, priceCache, chainId }: APIOptions) => { - const makeRequest = makeRequestFactory(logger) - - app.get('/tokens', (_: Request, res: Response) => rskExplorerApi.getTokens().then(res.status(200).json.bind(res))) + app.get('/tokens', (_: Request, res: Response, next: NextFunction) => rskExplorerApi.getTokens() + .then(res.status(200).json.bind(res)) + .catch(next) + ) app.get( '/address/:address/tokens', - ({ params: { address } }: Request, res: Response) => rskExplorerApi.getTokensByAddress(address) + ({ params: { address } }: Request, res: Response, next: NextFunction) => rskExplorerApi.getTokensByAddress(address) .then(responseJsonOk(res)) + .catch(next) ) app.get( '/address/:address/events', - ({ params: { address } }: Request, res: Response) => rskExplorerApi.getEventsByAddress(address) + ({ params: { address } }: Request, res: Response, next: NextFunction) => rskExplorerApi.getEventsByAddress(address) .then(responseJsonOk(res)) + .catch(next) ) app.get( '/address/:address/transactions', - ({ params: { address }, query: { limit, prev, next } }: Request, res: Response) => + ({ params: { address }, query: { limit, prev, next } }: Request, res: Response, nextFunction: NextFunction) => rskExplorerApi.getTransactionsByAddress( address, limit as string, prev as string, next as string ) .then(responseJsonOk(res)) + .catch(nextFunction) ) app.get( '/price', - async (req: Request<{}, {}, {}, PricesQueryParams>, res: Response) => makeRequest( - req, res, () => { - const addresses = req.query.addresses.split(',') - const convert = req.query.convert - validatePricesRequest(addresses, convert) - return coinMarketCapApi.getQuotesLatest({ addresses, convert }) - } - ) + (req: Request<{}, {}, {}, PricesQueryParams>, res: Response, next: NextFunction) => { + const addresses = req.query.addresses.split(',').filter((address) => isTokenSupported(address, chainId)) + const convert = req.query.convert + + if (!isConvertSupported(convert)) throw new Error('Convert not supported') + + const isAddressesEmpty = addresses.length === 0 + if (isAddressesEmpty) return responseJsonOk(res)({}) + + const { missingAddresses, pricesInCache } = findInCache(addresses, priceCache) + if (!missingAddresses.length) return responseJsonOk(res)(pricesInCache) + + const prices = coinMarketCapApi.getQuotesLatest({ addresses: missingAddresses, convert }) + prices + .then(pricesFromCMC => { + storeInCache(pricesFromCMC, priceCache) + const pricesRes = { + ...pricesInCache, + ...pricesFromCMC + } + return responseJsonOk(res)(pricesRes) + }) + .catch(next) + } ) app.get('/dapps', (_: Request, res: Response) => responseJsonOk(res)(registeredDapps)) + + app.use(errorHandler) } diff --git a/src/coinmatketcap/index.ts b/src/coinmarketcap/index.ts similarity index 61% rename from src/coinmatketcap/index.ts rename to src/coinmarketcap/index.ts index 298b507..a810579 100644 --- a/src/coinmatketcap/index.ts +++ b/src/coinmarketcap/index.ts @@ -1,20 +1,20 @@ import _axios, { AxiosResponse } from 'axios' import { ICoinMarketCapQuoteParams, ICoinMarketCapQuoteResponse } from './types' import { addressToCoinmarketcapId } from './support' +import { isTokenSupported } from './validations' import { Prices } from '../api/types' type PricesQueryParams = { addresses: string[], convert: string } -const coinmarketcapIdToAddress = Object.keys(addressToCoinmarketcapId) - .reduce((p, c) => ({ ...p, [addressToCoinmarketcapId[c]]: c }), {}) - -const fromQueryParamsToRequestParams = (params: PricesQueryParams): ICoinMarketCapQuoteParams => ({ - id: params.addresses.map(address => addressToCoinmarketcapId[address]).join(','), +const fromQueryParamsToRequestParams = (params: PricesQueryParams, chaindId: number): ICoinMarketCapQuoteParams => ({ + id: params.addresses + .filter((address) => isTokenSupported(address, chaindId)) + .map(address => addressToCoinmarketcapId[chaindId][address]).join(','), convert: params.convert }) const fromQuotesResponseToPrices = - (convert: string) => + (convert: string, coinmarketcapIdToAddress: Record) => (response: AxiosResponse) => Object.keys(response.data.data).reduce((p, c) => ({ ...p, @@ -28,23 +28,29 @@ export class CoinMarketCapAPI { headers: { 'X-CMC_PRO_API_KEY': string } baseURL: string axios: typeof _axios + chainId: number + coinmarketcapIdToAddress: Record constructor ( url: string, version: string, apiKey: string, - axios: typeof _axios + axios: typeof _axios, + chainId: number ) { this.baseURL = `${url}/${version}` this.headers = { 'X-CMC_PRO_API_KEY': apiKey } this.axios = axios + this.chainId = chainId + this.coinmarketcapIdToAddress = Object.keys(addressToCoinmarketcapId[chainId]) + .reduce((p, c) => ({ ...p, [addressToCoinmarketcapId[chainId][c]]: c }), {}) } getQuotesLatest = (queryParams: PricesQueryParams): Promise => this.axios.get( `${this.baseURL}/cryptocurrency/quotes/latest`, { headers: this.headers, - params: fromQueryParamsToRequestParams(queryParams) - }).then(fromQuotesResponseToPrices(queryParams.convert!)) + params: fromQueryParamsToRequestParams(queryParams, this.chainId) + }).then(fromQuotesResponseToPrices(queryParams.convert!, this.coinmarketcapIdToAddress)) } diff --git a/src/coinmarketcap/priceCache.ts b/src/coinmarketcap/priceCache.ts new file mode 100644 index 0000000..8021ba7 --- /dev/null +++ b/src/coinmarketcap/priceCache.ts @@ -0,0 +1,21 @@ +import NodeCache from 'node-cache' +import { Prices } from '../api/types' +import { IPriceCacheSearch } from './types' + +export const findInCache = (addresses: string[], cache: NodeCache): IPriceCacheSearch => { + const response: IPriceCacheSearch = { missingAddresses: [], pricesInCache: {} } + addresses.forEach(address => { + if (cache.has(address)) { + response.pricesInCache = { + ...response.pricesInCache, + ...cache.get(address) + } + } + }) + response.missingAddresses = addresses.filter(address => !Object.keys(response.pricesInCache).includes(address)) + return response +} + +export const storeInCache = (prices: Prices, cache: NodeCache): void => { + Object.keys(prices).forEach(address => cache.set(address, { [address]: prices[address] }), 60) +} diff --git a/src/coinmarketcap/support.ts b/src/coinmarketcap/support.ts new file mode 100644 index 0000000..ba5fc2d --- /dev/null +++ b/src/coinmarketcap/support.ts @@ -0,0 +1,17 @@ +export const addressToCoinmarketcapId = { + 30: { + '0x0000000000000000000000000000000000000000': '3626', // RBTC + '0x2acc95758f8b5f583470ba265eb685a8f45fc9d5': '3701', // RIF + '0xef213441a85df4d7acbdae0cf78004e1e486bb96': '825', // rUSDT + '0x4991516df6053121121274397a8c1dad608bc95b': '7785', // rBUND + '0xefc78fc7d48b64958315949279ba181c2114abbd': '8669' // SOV + }, + 31: { + '0x0000000000000000000000000000000000000000': '3626', // RBTC + '0x19f64674d8a5b4e652319f5e239efd3bc969a1fe': '3701', // tRIF + '0x4cfe225ce54c6609a525768b13f7d87432358c57': '825', // rKovUSDT + '0xe95afdfec031f7b9cd942eb7e60f053fb605dfcd': '7785' // rKovBUND + } +} + +export const supportedFiat = ['USD'] diff --git a/src/coinmatketcap/types.ts b/src/coinmarketcap/types.ts similarity index 91% rename from src/coinmatketcap/types.ts rename to src/coinmarketcap/types.ts index cac8cdc..dfc3806 100644 --- a/src/coinmatketcap/types.ts +++ b/src/coinmarketcap/types.ts @@ -1,3 +1,5 @@ +import { Prices } from '../api/types' + export interface ICoinMarketCapQuoteParams { id?: string; slug?: string; @@ -64,3 +66,8 @@ export interface ICoinMarketCapQuoteResponse { status: IStatus; data: ICryptocurrencyQuota; } + +export interface IPriceCacheSearch { + missingAddresses: string[]; + pricesInCache: Prices +} diff --git a/src/coinmarketcap/validations.ts b/src/coinmarketcap/validations.ts new file mode 100644 index 0000000..d522d79 --- /dev/null +++ b/src/coinmarketcap/validations.ts @@ -0,0 +1,9 @@ +import { addressToCoinmarketcapId, supportedFiat } from './support' + +export const isTokenSupported = (address: string, chainId: number) => addressToCoinmarketcapId[chainId][address] !== undefined +export const isConvertSupported = (convert: string) => supportedFiat.includes(convert) + +export const validatePricesRequest = (addresses: string[], convert: string, chainId: number) => { + addresses.forEach(address => { if (!isTokenSupported(address, chainId)) throw new Error('Token address not supported') }) + if (!isConvertSupported(convert)) throw new Error('Convert not supported') +} diff --git a/src/coinmatketcap/support.ts b/src/coinmatketcap/support.ts deleted file mode 100644 index ee0fd30..0000000 --- a/src/coinmatketcap/support.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const addressToCoinmarketcapId = { - '0x0000000000000000000000000000000000000000': '3626', // RBTC - '0x2acc95758f8b5f583470ba265eb685a8f45fc9d5': '3701' -} - -export const supportedFiat = ['USD'] diff --git a/src/coinmatketcap/validations.ts b/src/coinmatketcap/validations.ts deleted file mode 100644 index c04264f..0000000 --- a/src/coinmatketcap/validations.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { addressToCoinmarketcapId, supportedFiat } from './support' - -const isTokenSupported = (address: string) => addressToCoinmarketcapId[address] !== undefined -const isConvertSupported = (convert: string) => supportedFiat.includes(convert) - -export const validatePricesRequest = (addresses: string[], convert: string) => { - addresses.forEach(address => { if (!isTokenSupported(address)) throw new Error('Token address not supported') }) - if (!isConvertSupported(convert)) throw new Error('Convert not supported') -} diff --git a/src/index.ts b/src/index.ts index 740c0ce..7913ea3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,10 +2,16 @@ import 'dotenv/config' import express from 'express' import axios from 'axios' -import { CoinMarketCapAPI } from './coinmatketcap' +import { CoinMarketCapAPI } from './coinmarketcap' import { RSKExplorerAPI } from './rskExplorerApi' import { registeredDapps } from './registered_dapps' import { setupApi } from './api' +import { Server } from 'socket.io' +import NodeCache from 'node-cache' +import http from 'http' +import pushNewBalances from './subscriptions/pushNewBalances' +import pushNewPrices from './subscriptions/pushNewPrices' +import pushNewTransactions from './subscriptions/pushNewTransactions' const environment = { // TODO: remove these defaults @@ -16,21 +22,64 @@ const environment = { CHAIN_ID: parseInt(process.env.CHAIN_ID as string) || 31, COIN_MARKET_CAP_URL: process.env.COIN_MARKET_CAP_URL as string || 'https://pro-api.coinmarketcap.com', COIN_MARKET_CAP_VERSION: process.env.COIN_MARKET_CAP_VERSION as string || 'v1', - COIN_MARKET_CAP_KEY: process.env.COIN_MARKET_CAP_KEY! as string + COIN_MARKET_CAP_KEY: process.env.COIN_MARKET_CAP_KEY! as string, + DEFAULT_CONVERT_FIAT: process.env.DEFAULT_CONVERT_FIAT! as string } -const app = express() - const rskExplorerApi = new RSKExplorerAPI(environment.API_URL, environment.CHAIN_ID, axios) -const coinMarketCapApi = new CoinMarketCapAPI(environment.COIN_MARKET_CAP_URL, environment.COIN_MARKET_CAP_VERSION, environment.COIN_MARKET_CAP_KEY, axios) +const coinMarketCapApi = new CoinMarketCapAPI( + environment.COIN_MARKET_CAP_URL, + environment.COIN_MARKET_CAP_VERSION, + environment.COIN_MARKET_CAP_KEY, + axios, + environment.CHAIN_ID +) +const app = express() +const priceCache = new NodeCache() setupApi(app, { rskExplorerApi, coinMarketCapApi, registeredDapps, - logger: console + priceCache, + logger: console, + chainId: environment.CHAIN_ID +}) + +const server = http.createServer(app) +const io = new Server(server, { + // cors: { + // origin: 'https://amritb.github.io' + // }, + path: '/ws' +}) + +io.on('connection', (socket) => { + console.log('new user connected') + + socket.on('subscribe', ({ address }: { address: string }) => { + console.log('new subscription with address: ', address) + + const stopPushingNewBalances = pushNewBalances(socket, rskExplorerApi, address) + const stopPushingNewTransactions = pushNewTransactions(socket, rskExplorerApi, address) + const stopPushingNewPrices = pushNewPrices( + socket, + rskExplorerApi, + coinMarketCapApi, + address, + environment.DEFAULT_CONVERT_FIAT, + environment.CHAIN_ID, + priceCache + ) + + socket.on('disconnect', () => { + stopPushingNewBalances() + stopPushingNewTransactions() + stopPushingNewPrices() + }) + }) }) -app.listen(environment.PORT, () => { +server.listen(environment.PORT, () => { console.log(`RIF Wallet services running on port ${environment.PORT}.`) }) diff --git a/src/middleware/index.ts b/src/middleware/index.ts new file mode 100644 index 0000000..7809dd9 --- /dev/null +++ b/src/middleware/index.ts @@ -0,0 +1,17 @@ +import { ErrorRequestHandler } from 'express' + +export class CustomError extends Error { + status: number; + constructor (message: string, status: number) { + super(message) + this.status = status + } +} + +export const errorHandler: ErrorRequestHandler = (error, req, res, next) => { + const status = error.status || 500 + const message = error.message || 'Something went wrong' + console.error(error) + res.status(status).send(message) + next() +} diff --git a/src/rskExplorerApi/index.ts b/src/rskExplorerApi/index.ts index 081d16c..f57c8c0 100644 --- a/src/rskExplorerApi/index.ts +++ b/src/rskExplorerApi/index.ts @@ -52,9 +52,9 @@ export class RSKExplorerAPI { async getTransactionsByAddress ( address:string, - limit: string | undefined, - prev: string | undefined, - next: string | undefined + limit?: string | undefined, + prev?: string | undefined, + next?: string | undefined ) { const params = { module: 'transactions', diff --git a/src/rskExplorerApi/types.ts b/src/rskExplorerApi/types.ts index ecd4a80..5f776ab 100644 --- a/src/rskExplorerApi/types.ts +++ b/src/rskExplorerApi/types.ts @@ -78,3 +78,9 @@ export interface IApiTransactions { export interface TransactionsServerResponse { data: IApiTransactions[]; } + +export interface ChannelServerResponse { + channel: string; + action: string; + data: TransactionsServerResponse +} diff --git a/src/subscriptions/pushNewBalances.ts b/src/subscriptions/pushNewBalances.ts new file mode 100644 index 0000000..7cc4f2a --- /dev/null +++ b/src/subscriptions/pushNewBalances.ts @@ -0,0 +1,51 @@ +import { Socket } from 'socket.io' +import { DefaultEventsMap } from 'socket.io/dist/typed-events' +import { RSKExplorerAPI } from '../rskExplorerApi/index' + +interface ISentBalances { + [address: string]: { + [tokenAddress: string]: string + } +} + +const EXECUTION_INTERVAL = 60000 + +const sentBalances: ISentBalances = {} + +const pushNewBalances = ( + socket: Socket, + api: RSKExplorerAPI, + address: string +) => { + const execute = executeFactory(socket, api, address) + + execute() + + const timer = setInterval(execute, EXECUTION_INTERVAL) + + return () => { + clearInterval(timer) + sentBalances[address] = {} + } +} + +const executeFactory = ( + socket: Socket, + api: RSKExplorerAPI, + address: string +) => async () => { + if (!sentBalances[address]) { + sentBalances[address] = {} + } + + const tokens = await api.getTokensByAddress(address.toLowerCase()) + + for (const token of tokens) { + if (sentBalances[address][token.contractAddress] !== token.balance) { + sentBalances[address][token.contractAddress] = token.balance + socket.emit('change', { type: 'newBalance', payload: token }) + } + } +} + +export default pushNewBalances diff --git a/src/subscriptions/pushNewPrices.ts b/src/subscriptions/pushNewPrices.ts new file mode 100644 index 0000000..7640a4e --- /dev/null +++ b/src/subscriptions/pushNewPrices.ts @@ -0,0 +1,53 @@ +import { CoinMarketCapAPI } from '../coinmarketcap' +import { DefaultEventsMap } from 'socket.io/dist/typed-events' +import { RSKExplorerAPI } from '../rskExplorerApi/index' +import { Socket } from 'socket.io' +import { isTokenSupported } from '../coinmarketcap/validations' +import NodeCache from 'node-cache' +import { findInCache, storeInCache } from '../coinmarketcap/priceCache' + +const EXECUTION_INTERVAL = 60000 + +const pushNewPrices = (socket: Socket, + api: RSKExplorerAPI, + cmc: CoinMarketCapAPI, + address: string, + convert: string, + chainId: number, + priceCache: NodeCache +) => { + const execute = getPricesByToken(socket, api, cmc, address, convert, chainId, priceCache) + + execute() + + const timer = setInterval(execute, EXECUTION_INTERVAL) + + return () => { + clearInterval(timer) + } +} + +const getPricesByToken = ( + socket: Socket, + api: RSKExplorerAPI, + cmc: CoinMarketCapAPI, + address: string, + convert: string, + chainId: number, + priceCache: NodeCache) => async () => { + const RBTC = '0x0000000000000000000000000000000000000000' + const addresses = [RBTC, ...(await api.getTokensByAddress(address.toLowerCase())) + .map(token => token.contractAddress.toLocaleLowerCase()) + .filter(token => isTokenSupported(token, chainId))] + + const { missingAddresses, pricesInCache } = findInCache(addresses, priceCache) + if (!missingAddresses.length) return pricesInCache + + const prices = cmc.getQuotesLatest({ addresses: missingAddresses, convert }) + prices.then(pricesFromCMC => { + storeInCache(pricesFromCMC, priceCache) + socket.emit('change', { type: 'newPrice', payload: { ...pricesInCache, ...pricesFromCMC } }) + }) +} + +export default pushNewPrices diff --git a/src/subscriptions/pushNewTransactions.ts b/src/subscriptions/pushNewTransactions.ts new file mode 100644 index 0000000..85ad132 --- /dev/null +++ b/src/subscriptions/pushNewTransactions.ts @@ -0,0 +1,50 @@ +import { Socket } from 'socket.io' +import { DefaultEventsMap } from 'socket.io/dist/typed-events' +import { RSKExplorerAPI } from '../rskExplorerApi' + +let lastReceivedTransactionBlockNumber = -1 +const EXECUTION_INTERVAL = 60000 + +const pushNewTransactions = ( + socket: Socket, + api: RSKExplorerAPI, + address: string) => { + const execute = executeFactory(socket, api, address) + + const timer = setInterval(execute, EXECUTION_INTERVAL) + + // store the last transaction block number when user subscribes + api.getTransactionsByAddress(address.toLowerCase()).then(({ data }) => { + if (data.length) { + lastReceivedTransactionBlockNumber = data[0].blockNumber + } + }) + + return () => { + clearInterval(timer) + } +} + +const executeFactory = ( + socket: Socket, + api: RSKExplorerAPI, + address: string +) => async () => { + const { data } = await api.getTransactionsByAddress(address.toLowerCase()) + // assuming descendent order, if the first transaction has smaller block number + // than the last sent no transactions are pushed + if (data.length && data[0].blockNumber > lastReceivedTransactionBlockNumber) { + // push them in historical order + data.reverse().forEach(transaction => { + if (transaction.blockNumber > lastReceivedTransactionBlockNumber) { + console.log('change', { type: 'newTransaction', payload: transaction }) + socket.emit('change', { type: 'newTransaction', payload: transaction }) + } + }) + + // once finished pushing, update to the last transaction sent + lastReceivedTransactionBlockNumber = data[data.length - 1].blockNumber + } +} + +export default pushNewTransactions diff --git a/test/api.test.ts b/test/api.test.ts index d6a75bd..ad0cc4c 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -1,17 +1,22 @@ import express from 'express' +import NodeCache from 'node-cache' import request from 'supertest' import { setupApi } from '../src/api' -import { CoinMarketCapAPI } from '../src/coinmatketcap' -import { mockCoinMarketCap, pricesResponse } from './mockResponses' +import { CoinMarketCapAPI } from '../src/coinmarketcap' +import { mockCoinMarketCap, pricesResponse, pricesResponseForCaching, rifPriceFromCache, sovPriceFromCache } from './mockResponses' -const setupTestApi = (coinMarketCapApi: CoinMarketCapAPI) => { +import { CustomError } from '../src/middleware' + +const setupTestApi = (coinMarketCapApi: CoinMarketCapAPI, priceCache: NodeCache = new NodeCache()) => { const app = express() setupApi(app, { rskExplorerApi: {} as any, coinMarketCapApi, - registeredDapps: {} as any + registeredDapps: {} as any, + priceCache, + chainId: 30 }) return app @@ -36,8 +41,37 @@ describe('coin market cap', () => { expect(JSON.parse(text)).toEqual(pricesResponse) }) + test('valid response from cache', async () => { + const priceCache = new NodeCache() + priceCache.set('0xefc78fc7d48b64958315949279ba181c2114abbd', sovPriceFromCache) + const app = setupTestApi(coinMarketCapApiMock as any, priceCache) + + const { res: { text } } = await request(app) + .get('/price?convert=USD&addresses=0x0000000000000000000000000000000000000000,0x2acc95758f8b5f583470ba265eb685a8f45fc9d5,0xefc78fc7d48b64958315949279ba181c2114abbd') + .expect('Content-Type', /json/) + .expect(200) + + expect(getQuotesLatestMock).toHaveBeenCalledWith({ addresses: ['0x0000000000000000000000000000000000000000', '0x2acc95758f8b5f583470ba265eb685a8f45fc9d5'], convert: 'USD' }) + expect(JSON.parse(text)).toEqual(pricesResponseForCaching) + }) + + test('valid response with cache invalidated', async () => { + const priceCache = new NodeCache() + priceCache.set('0xefc78fc7d48b64958315949279ba181c2114abbd', rifPriceFromCache) + const app = setupTestApi(coinMarketCapApiMock as any, priceCache) + priceCache.flushAll() + + const { res: { text } } = await request(app) + .get('/price?convert=USD&addresses=0x0000000000000000000000000000000000000000,0x2acc95758f8b5f583470ba265eb685a8f45fc9d5') + .expect('Content-Type', /json/) + .expect(200) + + expect(getQuotesLatestMock).toHaveBeenCalledWith({ addresses: ['0x0000000000000000000000000000000000000000', '0x2acc95758f8b5f583470ba265eb685a8f45fc9d5'], convert: 'USD' }) + expect(JSON.parse(text)).toEqual(pricesResponse) + }) + test('handles error', async () => { - const getQuotesLatestThrowsMock = jest.fn(() => Promise.reject(new Error('error'))) + const getQuotesLatestThrowsMock = jest.fn(() => Promise.reject(new CustomError('error', 500))) const coinMarketCapApiThrowsMock = { getQuotesLatest: getQuotesLatestThrowsMock @@ -71,9 +105,9 @@ describe('coin market cap', () => { const res = await request(app) .get('/price?convert=USD&addresses=0x2acc95758f8b5f583470ba265eb685a8f45fc9d') - .expect(500) + .expect(200) - expect(res.text).toEqual('Token address not supported') + expect(res.text).toEqual('{}') expect(axiosMock.get).not.toHaveBeenCalled() }) }) diff --git a/test/mockResponses.ts b/test/mockResponses.ts index 66789e0..1920db0 100644 --- a/test/mockResponses.ts +++ b/test/mockResponses.ts @@ -1,4 +1,4 @@ -import { CoinMarketCapAPI } from '../src/coinmatketcap' +import { CoinMarketCapAPI } from '../src/coinmarketcap' const rbtcLastUpdated = '2021-12-18T03:51:07.000Z' const rbtcPrice = 46173.353546991406 @@ -6,6 +6,9 @@ const rbtcPrice = 46173.353546991406 const rifLastUpdated = '2021-12-18T03:50:08.000Z' const rifPrice = 0.1966674666988437 +const sovLastUpdated = '2022-01-18T03:50:08.000Z' +const sovPrice = 7.38 + export const coinmarketcapResponse = { status: { timestamp: '2021-12-18T03:52:09.880Z', @@ -110,6 +113,33 @@ export const pricesResponse = { } } +export const pricesResponseForCaching = { + '0x0000000000000000000000000000000000000000': { + price: rbtcPrice, + lastUpdated: rbtcLastUpdated + }, + '0x2acc95758f8b5f583470ba265eb685a8f45fc9d5': { + price: rifPrice, + lastUpdated: rifLastUpdated + }, + '0xefc78fc7d48b64958315949279ba181c2114abbd': { + price: sovPrice, + lastUpdated: sovLastUpdated + } +} + +export const rifPriceFromCache = { + '0x2acc95758f8b5f583470ba265eb685a8f45fc9d5': { + price: 0.2077774666988437, lastUpdated: '2021-12-08T03:50:08.000Z' + } +} + +export const sovPriceFromCache = { + '0xefc78fc7d48b64958315949279ba181c2114abbd': { + price: 7.38, lastUpdated: '2022-01-18T03:50:08.000Z' + } +} + export const mockCoinMarketCap = () => { const getQuotesLatestMock = jest.fn(() => Promise.resolve({ data: coinmarketcapResponse })) @@ -117,7 +147,7 @@ export const mockCoinMarketCap = () => { get: getQuotesLatestMock } - const coinMarketCapApi = new CoinMarketCapAPI('url', 'v1', 'api-key', axiosMock as any) + const coinMarketCapApi = new CoinMarketCapAPI('url', 'v1', 'api-key', axiosMock as any, 30) return { axiosMock, coinMarketCapApi } }