From c8f69e164f32d9c001e8823b27739964670ea592 Mon Sep 17 00:00:00 2001 From: Christian Escalante Date: Thu, 27 Jan 2022 12:20:42 -0300 Subject: [PATCH] Live balances, transactions and prices (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding CoinMarketCap types * CoinMarketCap class to access the quotas(prices) of each token or coin * Added type for query params for the prices endpoint * Changing return type * Changing return type * Adding price endpoint * Function to convert a new Date object into UTC epoch * Adding loki js to cache the amount of requests we have performed to the coinmarketcap api * Implementing lokijs to cache the request count and data from coinmarketcap * Removing cache system and lokijs * Adding types for metadata * Removing error handling from the coinmarketcap lib * Types for metadata response * Adding sanitization functions for metadata and quote results * Running linter * Endpoint for prices. * Reunning linter * socket setup * something proposal * better emit event name * Fixing types syntax * push new balances working * minor changes and handle disconnect * Move parsing logic to coinmarketcap.ts * Move files * Refactor api * Lint * Refactor json result * Add tests * Add error handling tests - move validatoins to api * review changes * Getting new transactions from WS explorer api (#11) * Lint * Polling function for coinmarketcap prices * Implemented pushNewPrices socket based on wallet address * Implementing tokens addresses by chainId, 30 for mainnet and 31 for testnet * Fixing tests * Adding chainId parameter for the coinmarketcap class * Lint * Fixing merge conflicts * Removing trailing comma * Extends map between contract address and coin market cap * Update list of supported tokens by coinmarketcap * Filtering out unsupported tokens * Commenting out the validatePricesRequest function which validates if the token is supported or not, since we are doing that inside the coinmarketcap function * Commenting out the validatePricesRequest function which validates if the token is supported or not, since we are doing that inside the coinmarketcap function * coinmarketcap api will fail if no ids passed therefore we added an if if there are no addresses * adding an empty json as default value for prices, in case the wallet doesnt have any token yet, the backend wont crash because of the coinmarket cap api no receiving tokens ids * Lint * Adding only supported tokens * exporting isConvertSupported function * Lint * Adding conditions to handle unsupported convert or addresses * Modifying tests to handle new responses * Adding chainId as parameter to the subscriptions * Adding RBTC as default in the subscriptions * Add caching strategy to coin market cap * Fixing test for caching * Migrate store in cache and filtering missing address * Fixing name in test and add type for channel response * Adding tests for caching * Polling to get contract call transactions (#19) * Enhancement/error handling (#17) * Custom error class and errorhandler functio * Removing errorhandler from here and using it in setupAPI * Uncommenting the asserts that checked the text of the error message response * Implementing next function in the error handlers of the promises * Update src/middleware/index.ts Co-authored-by: Ilan <36084092+ilanolkies@users.noreply.github.com> * Update src/middleware/index.ts Co-authored-by: Ilan <36084092+ilanolkies@users.noreply.github.com> * Update src/api/index.ts Co-authored-by: Ilan <36084092+ilanolkies@users.noreply.github.com> * applying suggestions Co-authored-by: Agustin Villalobos Co-authored-by: Ilan <36084092+ilanolkies@users.noreply.github.com> * Push transactions that have higher block number than the last sent Co-authored-by: Agustin Villalobos V Co-authored-by: Christian Escalante Co-authored-by: Ilan Co-authored-by: Agustín Villalobos Co-authored-by: sleyter93 <96137983+sleyter93@users.noreply.github.com> Co-authored-by: Agustin Villalobos Co-authored-by: Sleyter Sandoval Co-authored-by: Sleyter Sandoval Co-authored-by: Ilan <36084092+ilanolkies@users.noreply.github.com> Co-authored-by: Agustín Villalobos <32603375+agustin-v@users.noreply.github.com> --- package-lock.json | 756 +++++++++++++++++- package.json | 4 +- src/api/index.ts | 74 +- src/{coinmatketcap => coinmarketcap}/index.ts | 24 +- src/coinmarketcap/priceCache.ts | 21 + src/coinmarketcap/support.ts | 17 + src/{coinmatketcap => coinmarketcap}/types.ts | 7 + src/coinmarketcap/validations.ts | 9 + src/coinmatketcap/support.ts | 6 - src/coinmatketcap/validations.ts | 9 - src/index.ts | 63 +- src/middleware/index.ts | 17 + src/rskExplorerApi/index.ts | 6 +- src/rskExplorerApi/types.ts | 6 + src/subscriptions/pushNewBalances.ts | 51 ++ src/subscriptions/pushNewPrices.ts | 53 ++ src/subscriptions/pushNewTransactions.ts | 50 ++ test/api.test.ts | 48 +- test/mockResponses.ts | 34 +- 19 files changed, 1173 insertions(+), 82 deletions(-) rename src/{coinmatketcap => coinmarketcap}/index.ts (61%) create mode 100644 src/coinmarketcap/priceCache.ts create mode 100644 src/coinmarketcap/support.ts rename src/{coinmatketcap => coinmarketcap}/types.ts (91%) create mode 100644 src/coinmarketcap/validations.ts delete mode 100644 src/coinmatketcap/support.ts delete mode 100644 src/coinmatketcap/validations.ts create mode 100644 src/middleware/index.ts create mode 100644 src/subscriptions/pushNewBalances.ts create mode 100644 src/subscriptions/pushNewPrices.ts create mode 100644 src/subscriptions/pushNewTransactions.ts 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 } }