From 5f0c140046ffd32c6c1d524da453bc289e8190c0 Mon Sep 17 00:00:00 2001
From: Anthony Potdevin <31413433+apotdevin@users.noreply.github.com>
Date: Sun, 30 Jan 2022 22:07:05 +0100
Subject: [PATCH] chore: ws (#394)
---
.env | 10 +
package-lock.json | 663 +++++++++++++++++-
package.json | 5 +
src/client/pages/_app.tsx | 26 +-
src/client/src/components/generic/helpers.tsx | 2 +-
.../components/gridWrapper/GridWrapper.tsx | 3 -
.../components/statusCheck/StatusCheck.tsx | 24 -
.../toastContainer/ToastContainer.ts | 17 -
src/client/src/context/SocketContext.tsx | 115 +++
src/client/src/hooks/UseListener.tsx | 321 +++++++++
src/client/src/hooks/UseNodeBalances.tsx | 4 +-
src/client/src/hooks/UseSocket.tsx | 51 ++
.../src/views/balance/AdvancedBalance.tsx | 121 ++--
src/client/src/views/balance/Logs.tsx | 83 +++
src/server/app.module.ts | 5 +
src/server/config/configuration.ts | 20 +
src/server/modules/api/bos/bos.module.ts | 3 +-
src/server/modules/api/bos/bos.resolver.ts | 30 +-
src/server/modules/auth/auth.module.ts | 20 +
src/server/modules/auth/auth.service.ts | 23 +
src/server/modules/node/lnd/lnd.helpers.ts | 4 +
src/server/modules/sub/sub.module.ts | 10 +
src/server/modules/sub/sub.service.ts | 433 ++++++++++++
src/server/modules/ws/ws.gateway.ts | 68 ++
src/server/modules/ws/ws.module.ts | 11 +
src/server/modules/ws/ws.service.ts | 16 +
src/server/utils/string.ts | 17 +
27 files changed, 1989 insertions(+), 116 deletions(-)
delete mode 100644 src/client/src/components/statusCheck/StatusCheck.tsx
delete mode 100644 src/client/src/components/toastContainer/ToastContainer.ts
create mode 100644 src/client/src/context/SocketContext.tsx
create mode 100644 src/client/src/hooks/UseListener.tsx
create mode 100644 src/client/src/hooks/UseSocket.tsx
create mode 100644 src/client/src/views/balance/Logs.tsx
create mode 100644 src/server/modules/auth/auth.module.ts
create mode 100644 src/server/modules/auth/auth.service.ts
create mode 100644 src/server/modules/sub/sub.module.ts
create mode 100644 src/server/modules/sub/sub.service.ts
create mode 100644 src/server/modules/ws/ws.gateway.ts
create mode 100644 src/server/modules/ws/ws.module.ts
create mode 100644 src/server/modules/ws/ws.service.ts
diff --git a/.env b/.env
index 47a28924..b507a0d3 100644
--- a/.env
+++ b/.env
@@ -25,6 +25,16 @@
# THEME='dark'
# CURRENCY='sat'
+# -----------
+# Subscription Configs
+# -----------
+# DISABLE_ALL_SUBS=true
+# DISABLE_INVOICE_SUB=true
+# DISABLE_PAYMENT_SUB=true
+# DISABLE_FORWARD_SUB=true
+# DISABLE_CHANNEL_SUB=true
+# DISABLE_BACKUP_SUB=true
+
# -----------
# Privacy Configs
# -----------
diff --git a/package-lock.json b/package-lock.json
index de14a9d5..f89891b0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,9 +14,13 @@
"@nestjs/config": "^1.1.6",
"@nestjs/core": "^8.2.6",
"@nestjs/graphql": "^9.1.2",
+ "@nestjs/jwt": "^8.0.0",
"@nestjs/passport": "^8.1.0",
"@nestjs/platform-express": "^8.2.6",
+ "@nestjs/platform-socket.io": "^8.2.6",
"@nestjs/throttler": "^2.0.0",
+ "@nestjs/websockets": "^8.2.6",
+ "@types/socket.io": "^3.0.2",
"@visx/axis": "^2.6.0",
"@visx/chord": "^2.1.2",
"@visx/event": "^2.6.0",
@@ -73,6 +77,7 @@
"rimraf": "^3.0.2",
"rxjs": "^7.5.2",
"secp256k1": "^4.0.3",
+ "socket.io-client": "^4.4.1",
"socks-proxy-agent": "^6.1.1",
"styled-components": "^5.3.3",
"styled-react-modal": "^2.1.0",
@@ -4945,6 +4950,26 @@
}
}
},
+ "node_modules/@nestjs/jwt": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-8.0.0.tgz",
+ "integrity": "sha512-fz2LQgYY2zmuD8S+8UE215anwKyXlnB/1FwJQLVR47clNfMeFMK8WCxmn6xdPhF5JKuV1crO6FVabb1qWzDxqQ==",
+ "dependencies": {
+ "@types/jsonwebtoken": "8.5.4",
+ "jsonwebtoken": "8.5.1"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@nestjs/jwt/node_modules/@types/jsonwebtoken": {
+ "version": "8.5.4",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz",
+ "integrity": "sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@nestjs/mapped-types": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.0.tgz",
@@ -4985,6 +5010,24 @@
"@nestjs/core": "^8.0.0"
}
},
+ "node_modules/@nestjs/platform-socket.io": {
+ "version": "8.2.6",
+ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-8.2.6.tgz",
+ "integrity": "sha512-Gkay52E6PmhuL1e1EF1GnJuivt4NxXaqY2I3VV4LF4X2jNHmL09EAyJofs1G6ySF8QUtR9HqtW/+ohxr7RipsQ==",
+ "dependencies": {
+ "socket.io": "4.4.1",
+ "tslib": "2.3.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nest"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^8.0.0",
+ "@nestjs/websockets": "^8.0.0",
+ "rxjs": "^7.1.0"
+ }
+ },
"node_modules/@nestjs/schematics": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-8.0.5.tgz",
@@ -5127,6 +5170,28 @@
"reflect-metadata": "^0.1.13"
}
},
+ "node_modules/@nestjs/websockets": {
+ "version": "8.2.6",
+ "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-8.2.6.tgz",
+ "integrity": "sha512-GUdPd5X+ojNeaYE+/4c4105tb8skvQt3KyR7CCzrhMziHsRakDLQ/8LO932fh8ADrkRxr7jfAbt3UTq5FggQ2w==",
+ "dependencies": {
+ "iterare": "1.2.1",
+ "object-hash": "2.2.0",
+ "tslib": "2.3.1"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^8.0.0",
+ "@nestjs/core": "^8.0.0",
+ "@nestjs/platform-socket.io": "^8.0.0",
+ "reflect-metadata": "^0.1.12",
+ "rxjs": "^7.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@nestjs/platform-socket.io": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@next/env": {
"version": "12.0.8",
"resolved": "https://registry.npmjs.org/@next/env/-/env-12.0.8.tgz",
@@ -5611,6 +5676,19 @@
"@sinonjs/commons": "^1.7.0"
}
},
+ "node_modules/@socket.io/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz",
+ "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q=="
+ },
"node_modules/@szmarczak/http-timer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
@@ -5730,6 +5808,11 @@
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
"integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w=="
},
+ "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",
@@ -5741,8 +5824,7 @@
"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==",
- "dev": true
+ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"node_modules/@types/cookiejar": {
"version": "2.1.2",
@@ -6158,6 +6240,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/socket.io": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz",
+ "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==",
+ "deprecated": "This is a stub types definition. socket.io provides its own type definitions, so you do not need this installed.",
+ "dependencies": {
+ "socket.io": "*"
+ }
+ },
"node_modules/@types/stack-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -8235,6 +8326,14 @@
}
]
},
+ "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/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
@@ -9370,8 +9469,7 @@
"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/concat-map": {
"version": "0.0.1",
@@ -10780,6 +10878,135 @@
"once": "^1.4.0"
}
},
+ "node_modules/engine.io": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz",
+ "integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==",
+ "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": "6.1.1",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.1.1.tgz",
+ "integrity": "sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.0.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.0.0",
+ "has-cors": "1.1.0",
+ "parseqs": "0.0.6",
+ "parseuri": "0.0.6",
+ "ws": "~8.2.3",
+ "xmlhttprequest-ssl": "~2.0.0",
+ "yeast": "0.1.2"
+ }
+ },
+ "node_modules/engine.io-client/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-client/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-client/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/engine.io-parser": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz",
+ "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==",
+ "dependencies": {
+ "@socket.io/base64-arraybuffer": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "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/enhanced-resolve": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz",
@@ -13521,6 +13748,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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",
@@ -20709,6 +20941,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",
@@ -22835,6 +23077,131 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/socket.io": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz",
+ "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==",
+ "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": "4.4.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.4.1.tgz",
+ "integrity": "sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.0.0",
+ "backo2": "~1.0.2",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.1.1",
+ "parseuri": "0.0.6",
+ "socket.io-parser": "~4.1.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-client/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-client/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-client/node_modules/socket.io-parser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.1.tgz",
+ "integrity": "sha512-USQVLSkDWE5nbcY760ExdKaJxCE65kcsG/8k5FDGZVVxpD1pA7hABYXYkCUvxUuYYh/+uQw0N/fvBzfT8o07KA==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.0.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "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/socks": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz",
@@ -25718,6 +26085,14 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true
},
+ "node_modules/xmlhttprequest-ssl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
+ "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/xss": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.10.tgz",
@@ -25799,6 +26174,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",
@@ -29435,6 +29815,25 @@
}
}
},
+ "@nestjs/jwt": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-8.0.0.tgz",
+ "integrity": "sha512-fz2LQgYY2zmuD8S+8UE215anwKyXlnB/1FwJQLVR47clNfMeFMK8WCxmn6xdPhF5JKuV1crO6FVabb1qWzDxqQ==",
+ "requires": {
+ "@types/jsonwebtoken": "8.5.4",
+ "jsonwebtoken": "8.5.1"
+ },
+ "dependencies": {
+ "@types/jsonwebtoken": {
+ "version": "8.5.4",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz",
+ "integrity": "sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg==",
+ "requires": {
+ "@types/node": "*"
+ }
+ }
+ }
+ },
"@nestjs/mapped-types": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.0.tgz",
@@ -29459,6 +29858,15 @@
"tslib": "2.3.1"
}
},
+ "@nestjs/platform-socket.io": {
+ "version": "8.2.6",
+ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-8.2.6.tgz",
+ "integrity": "sha512-Gkay52E6PmhuL1e1EF1GnJuivt4NxXaqY2I3VV4LF4X2jNHmL09EAyJofs1G6ySF8QUtR9HqtW/+ohxr7RipsQ==",
+ "requires": {
+ "socket.io": "4.4.1",
+ "tslib": "2.3.1"
+ }
+ },
"@nestjs/schematics": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-8.0.5.tgz",
@@ -29552,6 +29960,16 @@
"md5": "^2.2.1"
}
},
+ "@nestjs/websockets": {
+ "version": "8.2.6",
+ "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-8.2.6.tgz",
+ "integrity": "sha512-GUdPd5X+ojNeaYE+/4c4105tb8skvQt3KyR7CCzrhMziHsRakDLQ/8LO932fh8ADrkRxr7jfAbt3UTq5FggQ2w==",
+ "requires": {
+ "iterare": "1.2.1",
+ "object-hash": "2.2.0",
+ "tslib": "2.3.1"
+ }
+ },
"@next/env": {
"version": "12.0.8",
"resolved": "https://registry.npmjs.org/@next/env/-/env-12.0.8.tgz",
@@ -29877,6 +30295,16 @@
"@sinonjs/commons": "^1.7.0"
}
},
+ "@socket.io/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ=="
+ },
+ "@socket.io/component-emitter": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz",
+ "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q=="
+ },
"@szmarczak/http-timer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
@@ -29990,6 +30418,11 @@
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
"integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w=="
},
+ "@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",
@@ -30001,8 +30434,7 @@
"@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==",
- "dev": true
+ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"@types/cookiejar": {
"version": "2.1.2",
@@ -30416,6 +30848,14 @@
"@types/node": "*"
}
},
+ "@types/socket.io": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz",
+ "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==",
+ "requires": {
+ "socket.io": "*"
+ }
+ },
"@types/stack-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -32085,6 +32525,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
+ "base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
+ },
"basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
@@ -32974,8 +33419,7 @@
"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=="
},
"concat-map": {
"version": "0.0.1",
@@ -34132,6 +34576,89 @@
"once": "^1.4.0"
}
},
+ "engine.io": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz",
+ "integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==",
+ "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": {
+ "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": "6.1.1",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.1.1.tgz",
+ "integrity": "sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==",
+ "requires": {
+ "@socket.io/component-emitter": "~3.0.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.0.0",
+ "has-cors": "1.1.0",
+ "parseqs": "0.0.6",
+ "parseuri": "0.0.6",
+ "ws": "~8.2.3",
+ "xmlhttprequest-ssl": "~2.0.0",
+ "yeast": "0.1.2"
+ },
+ "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=="
+ },
+ "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-parser": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz",
+ "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==",
+ "requires": {
+ "@socket.io/base64-arraybuffer": "~1.0.2"
+ }
+ },
"enhanced-resolve": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz",
@@ -36225,6 +36752,11 @@
"integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
"dev": true
},
+ "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",
@@ -41719,6 +42251,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",
@@ -43376,6 +43918,101 @@
"tslib": "^2.0.3"
}
},
+ "socket.io": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz",
+ "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==",
+ "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": "4.4.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.4.1.tgz",
+ "integrity": "sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ==",
+ "requires": {
+ "@socket.io/component-emitter": "~3.0.0",
+ "backo2": "~1.0.2",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.1.1",
+ "parseuri": "0.0.6",
+ "socket.io-parser": "~4.1.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=="
+ },
+ "socket.io-parser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.1.tgz",
+ "integrity": "sha512-USQVLSkDWE5nbcY760ExdKaJxCE65kcsG/8k5FDGZVVxpD1pA7hABYXYkCUvxUuYYh/+uQw0N/fvBzfT8o07KA==",
+ "requires": {
+ "@socket.io/component-emitter": "~3.0.0",
+ "debug": "~4.3.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=="
+ }
+ }
+ },
"socks": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz",
@@ -45524,6 +46161,11 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true
},
+ "xmlhttprequest-ssl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
+ "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
+ },
"xss": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.10.tgz",
@@ -45586,6 +46228,11 @@
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="
},
+ "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 54aced67..14cc921f 100644
--- a/package.json
+++ b/package.json
@@ -43,9 +43,13 @@
"@nestjs/config": "^1.1.6",
"@nestjs/core": "^8.2.6",
"@nestjs/graphql": "^9.1.2",
+ "@nestjs/jwt": "^8.0.0",
"@nestjs/passport": "^8.1.0",
"@nestjs/platform-express": "^8.2.6",
+ "@nestjs/platform-socket.io": "^8.2.6",
"@nestjs/throttler": "^2.0.0",
+ "@nestjs/websockets": "^8.2.6",
+ "@types/socket.io": "^3.0.2",
"@visx/axis": "^2.6.0",
"@visx/chord": "^2.1.2",
"@visx/event": "^2.6.0",
@@ -102,6 +106,7 @@
"rimraf": "^3.0.2",
"rxjs": "^7.5.2",
"secp256k1": "^4.0.3",
+ "socket.io-client": "^4.4.1",
"socks-proxy-agent": "^6.1.1",
"styled-components": "^5.3.3",
"styled-react-modal": "^2.1.0",
diff --git a/src/client/pages/_app.tsx b/src/client/pages/_app.tsx
index b2163e7f..04089122 100644
--- a/src/client/pages/_app.tsx
+++ b/src/client/pages/_app.tsx
@@ -1,8 +1,6 @@
+import { useEffect } from 'react';
import styled, { ThemeProvider } from 'styled-components';
import { ModalProvider, BaseModalBackground } from 'styled-react-modal';
-import { useRouter } from 'next/router';
-import Head from 'next/head';
-import { StyledToastContainer } from '../src/components/toastContainer/ToastContainer';
import { AppProps } from 'next/app';
import { ApolloProvider } from '@apollo/client';
import { useApollo } from '../config/client';
@@ -13,13 +11,17 @@ import { GlobalStyles } from '../src/styles/GlobalStyle';
import { Header } from '../src/layouts/header/Header';
import { Footer } from '../src/layouts/footer/Footer';
import { PageWrapper, HeaderBodyWrapper } from '../src/layouts/Layout.styled';
+import { ToastContainer } from 'react-toastify';
+import { useListener } from '../src/hooks/UseListener';
+import { SocketProvider } from '../src/context/SocketContext';
+import { useRouter } from 'next/router';
import getConfig from 'next/config';
+import Head from 'next/head';
import 'react-toastify/dist/ReactToastify.min.css';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import 'react-circular-progressbar/dist/styles.css';
-import { useEffect } from 'react';
const { publicRuntimeConfig } = getConfig();
const { logoutUrl } = publicRuntimeConfig;
@@ -57,6 +59,8 @@ const Wrapper: React.FC<{ authenticated: boolean }> = ({
const isRoot = pathname === '/login' || pathname === '/sso';
+ useListener(isRoot);
+
return (
@@ -67,6 +71,7 @@ const Wrapper: React.FC<{ authenticated: boolean }> = ({
{authenticated ? children : }
+
@@ -91,14 +96,15 @@ export default function App({ Component, pageProps }: AppProps) {
-
-
-
-
-
+
+
+
+
+
+
+
-
);
}
diff --git a/src/client/src/components/generic/helpers.tsx b/src/client/src/components/generic/helpers.tsx
index 2a8be9eb..713f5a64 100644
--- a/src/client/src/components/generic/helpers.tsx
+++ b/src/client/src/components/generic/helpers.tsx
@@ -208,7 +208,7 @@ export const getStatusDot = (status: boolean, type: string) => {
};
export const renderLine = (
- title: string,
+ title: string | number,
content: number | string | JSX.Element | undefined | null,
key?: string | number,
deleteCallback?: () => void
diff --git a/src/client/src/components/gridWrapper/GridWrapper.tsx b/src/client/src/components/gridWrapper/GridWrapper.tsx
index dc78c86c..a198654e 100644
--- a/src/client/src/components/gridWrapper/GridWrapper.tsx
+++ b/src/client/src/components/gridWrapper/GridWrapper.tsx
@@ -5,7 +5,6 @@ import { BitcoinPrice } from '../../../src/components/bitcoinInfo/BitcoinPrice';
import { mediaWidths } from '../../styles/Themes';
import { Section } from '../section/Section';
import { Navigation } from '../../layouts/navigation/Navigation';
-import { StatusCheck } from '../statusCheck/StatusCheck';
type GridProps = {
noNavigation?: boolean;
@@ -41,7 +40,6 @@ export const GridWrapper: React.FC = ({
-
{!noNavigation && }
{centerContent ? (
@@ -58,7 +56,6 @@ export const SimpleWrapper: React.FC = ({ children }) => (
);
diff --git a/src/client/src/components/statusCheck/StatusCheck.tsx b/src/client/src/components/statusCheck/StatusCheck.tsx
deleted file mode 100644
index 3706cc9d..00000000
--- a/src/client/src/components/statusCheck/StatusCheck.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useEffect } from 'react';
-import { toast } from 'react-toastify';
-import getConfig from 'next/config';
-import { useGetNodeInfoQuery } from '../../../src/graphql/queries/__generated__/getNodeInfo.generated';
-
-const { publicRuntimeConfig } = getConfig();
-const { logoutUrl, basePath } = publicRuntimeConfig;
-
-export const StatusCheck: React.FC = () => {
- const { error, stopPolling } = useGetNodeInfoQuery({
- fetchPolicy: 'network-only',
- pollInterval: 10000,
- });
-
- useEffect(() => {
- if (error) {
- toast.error(`Unable to connect to node`);
- stopPolling();
- window.location.href = logoutUrl || `${basePath}/login`;
- }
- }, [error, stopPolling]);
-
- return null;
-};
diff --git a/src/client/src/components/toastContainer/ToastContainer.ts b/src/client/src/components/toastContainer/ToastContainer.ts
deleted file mode 100644
index 2f3fa1d4..00000000
--- a/src/client/src/components/toastContainer/ToastContainer.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import styled from 'styled-components';
-import { ToastContainer } from 'react-toastify';
-
-export const StyledToastContainer = styled(ToastContainer)`
- .Toastify__toast {
- border-radius: 4px;
- }
- .Toastify__toast--error {
- border-radius: 4px;
- }
- .Toastify__toast--warning {
- border-radius: 4px;
- }
- .Toastify__toast--success {
- border-radius: 4px;
- }
-`;
diff --git a/src/client/src/context/SocketContext.tsx b/src/client/src/context/SocketContext.tsx
new file mode 100644
index 00000000..f48a7f29
--- /dev/null
+++ b/src/client/src/context/SocketContext.tsx
@@ -0,0 +1,115 @@
+import React, { FC, useCallback, useRef, useState } from 'react';
+import io from 'socket.io-client';
+import { Socket } from 'socket.io-client';
+import getConfig from 'next/config';
+
+const { publicRuntimeConfig } = getConfig();
+const { basePath } = publicRuntimeConfig;
+
+type Connection = {
+ socket: Socket | undefined;
+ cleanup: () => void;
+};
+
+type CreateConnection = () => Connection;
+
+type Status = 'connecting' | 'connected' | 'disconnected';
+
+type Context = {
+ createConnection: CreateConnection;
+ getConnection: () => Socket | undefined;
+ getLastMessage: (forEvent: string) => any;
+ setLastMessage: (forEvent: string, message: any) => void;
+ registerSharedListener: (forEvent: string) => void;
+ getError: () => any;
+ setError: (error: any) => void;
+ getStatus: () => Status;
+};
+
+const SocketContext = React.createContext(undefined);
+
+const SocketProvider: FC<{ authToken?: string }> = ({
+ children,
+ authToken,
+}) => {
+ const sockets = useRef(undefined);
+
+ const [status, setStatus] = useState('disconnected');
+ const [error, setError] = useState(undefined);
+
+ const [lastMessages, setLastMessages] = useState>({});
+
+ const createConnection = useCallback(() => {
+ const cleanup = () => {
+ sockets.current?.disconnect();
+ };
+
+ // Early return if the user has no authToken cookie
+ if (!authToken) {
+ return { socket: undefined, cleanup };
+ }
+
+ if (sockets.current) {
+ sockets.current.connect();
+ return { socket: sockets.current, cleanup };
+ }
+
+ const handleConnect = () => setStatus('connected');
+ const handleDisconnect = () => setStatus('disconnected');
+
+ const socket = io({
+ ...(basePath ? { path: `${basePath}/socket.io` } : {}),
+ reconnectionAttempts: 5,
+ });
+
+ sockets.current = socket;
+
+ socket.on('error', (error: any) => setError(error));
+ socket.on('connect', handleConnect);
+ socket.on('disconnect', handleDisconnect);
+
+ return { socket, cleanup };
+ }, []);
+
+ const getLastMessage = (forEvent = '') => lastMessages[forEvent];
+ const setLastMessage = (forEvent: string, message: any) =>
+ setLastMessages(state => ({
+ ...state,
+ [forEvent]: message,
+ }));
+
+ const getConnection = () => sockets.current;
+ const getStatus = () => status;
+ const getError = () => error;
+
+ const registerSharedListener = (forEvent = '') => {
+ if (!sockets.current) return;
+ if (sockets.current.hasListeners(forEvent)) return;
+
+ sockets.current.on(forEvent, (message: any) =>
+ setLastMessages(state => ({
+ ...state,
+ [forEvent]: message,
+ }))
+ );
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export { SocketProvider, SocketContext };
diff --git a/src/client/src/hooks/UseListener.tsx b/src/client/src/hooks/UseListener.tsx
new file mode 100644
index 00000000..6b3c2f51
--- /dev/null
+++ b/src/client/src/hooks/UseListener.tsx
@@ -0,0 +1,321 @@
+import { useApolloClient } from '@apollo/client';
+import { useCallback, useEffect, useRef } from 'react';
+import { toast } from 'react-toastify';
+import {
+ getNodeLink,
+ getTransactionLink,
+ renderLine,
+} from '../components/generic/helpers';
+import { Separation } from '../components/generic/Styled';
+import { formatSats } from '../utils/helpers';
+import { useSocket, useSocketEvent } from './UseSocket';
+
+const refetchTimeMs = 1000 * 1;
+
+const options: { autoClose: false } = {
+ autoClose: false,
+};
+
+const renderToast = (
+ title: string,
+ content: JSX.Element | null | string | number
+) => {
+ return (
+ <>
+ {title}
+
+ {content}
+ >
+ );
+};
+
+export const useListener = (disabled?: boolean) => {
+ const refetchQueryTimeout: { current: ReturnType | null } =
+ useRef(null);
+
+ const client = useApolloClient();
+
+ const { socket } = useSocket(disabled);
+
+ const invoice = useSocketEvent(socket, 'invoice_updated');
+ const payment = useSocketEvent(socket, 'payment');
+ const forward = useSocketEvent(socket, 'forward');
+ // const change = useSocketEvent(socket, 'channel_active_changed');
+ const closed = useSocketEvent(socket, 'channel_closed');
+ const opened = useSocketEvent(socket, 'channel_opened');
+ const opening = useSocketEvent(socket, 'channel_opening');
+
+ const handleRefetchQueries = useCallback(
+ (extra: string[] = []) => {
+ if (!!refetchQueryTimeout.current) {
+ clearTimeout(refetchQueryTimeout.current);
+ }
+
+ refetchQueryTimeout.current = setTimeout(async () => {
+ client.refetchQueries({
+ include: ['GetNodeBalances', 'GetNodeInfo', ...extra],
+ });
+ }, refetchTimeMs);
+ },
+ [client]
+ );
+
+ useEffect(() => {
+ return () => {
+ if (!!refetchQueryTimeout.current) {
+ clearTimeout(refetchQueryTimeout.current);
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!invoice.lastMessage) return;
+ const { tokens, is_confirmed, description, description_hash, received } =
+ invoice.lastMessage;
+ if (is_confirmed) {
+ toast.success(
+ renderToast(
+ 'Invoice Paid',
+ <>
+ {renderLine('Description', description)}
+ {renderLine('Description Hash', description_hash)}
+ {renderLine('Amount', formatSats(received))}
+ >
+ ),
+ options
+ );
+ } else {
+ toast.info(
+ renderToast(
+ 'New Invoice Created',
+ <>
+ {renderLine('Description', description)}
+ {renderLine('Description Hash', description_hash)}
+ {renderLine('Amount', formatSats(tokens))}
+ >
+ ),
+ options
+ );
+ }
+ handleRefetchQueries(['GetInvoices']);
+ }, [invoice.lastMessage, handleRefetchQueries]);
+
+ useEffect(() => {
+ if (!payment.lastMessage) return;
+
+ const { hops, fee, destination, tokens } = payment.lastMessage;
+
+ const hopLines = hops.map((h: any, index: number) =>
+ renderLine(`Hop ${index + 1}`, h.channel)
+ );
+
+ toast.success(
+ renderToast(
+ 'New Payment',
+ <>
+ {renderLine('Destination', getNodeLink(destination))}
+ {renderLine('Amount', formatSats(tokens))}
+ {renderLine('Fee', fee ? formatSats(fee) : null)}
+ {hopLines}
+ >
+ ),
+ options
+ );
+ handleRefetchQueries(['GetPayments']);
+ }, [payment.lastMessage, handleRefetchQueries]);
+
+ useEffect(() => {
+ if (!forward.lastMessage) return;
+
+ const {
+ is_confirmed,
+ // is_failed,
+ is_receive,
+ is_send,
+ out_channel,
+ fee,
+ in_channel,
+ tokens,
+ } = forward.lastMessage;
+
+ // if (is_send && is_confirmed) {
+ // toast.success(
+ // renderToast(
+ // 'New Payment',
+ // <>
+ // {renderLine('Out Channel', out_channel)}
+ // {renderLine('Fee', fee ? formatSats(fee) : null)}
+ // >
+ // )
+ // );
+ // }
+
+ if (is_send || is_receive) return;
+
+ if (!is_confirmed) {
+ toast.warn(
+ renderToast(
+ 'Forward Attempt',
+ <>
+ {renderLine('In Channel', in_channel)}
+ {renderLine('Out Channel', out_channel)}
+ {renderLine('Tokens', formatSats(tokens))}
+ {renderLine('Fee', fee ? formatSats(fee) : null)}
+ >
+ ),
+ options
+ );
+ }
+
+ // if (is_failed) {
+ // toast.error(
+ // renderToast(
+ // 'Forward Failed',
+ // <>
+ // {renderLine('In Channel', in_channel)}
+ // {renderLine('Out Channel', out_channel)}
+ // {renderLine('Tokens', formatSats(tokens))}
+ // {renderLine('Fee', fee ? formatSats(fee) : null)}
+ // >
+ // )
+ // );
+ // }
+
+ if (is_confirmed) {
+ toast.success(
+ renderToast(
+ 'Successful Forward',
+ <>
+ {renderLine('In Channel', in_channel)}
+ {renderLine('Out Channel', out_channel)}
+ {renderLine('Fee', fee ? formatSats(fee) : null)}
+ >
+ ),
+ options
+ );
+ handleRefetchQueries(['GetForwards']);
+ }
+ }, [forward.lastMessage, handleRefetchQueries]);
+
+ // useEffect(() => {
+ // if (!change.lastMessage) return;
+ // toast.info('Channel Active Change');
+ // handleRefetchQueries();
+ // }, [change.lastMessage, handleRefetchQueries]);
+
+ useEffect(() => {
+ if (!closed.lastMessage) return;
+ const {
+ capacity,
+ close_transaction_id,
+ id,
+ is_breach_close,
+ is_cooperative_close,
+ is_funding_cancel,
+ is_local_force_close,
+ is_remote_force_close,
+ partner_public_key,
+ transaction_id,
+ } = closed.lastMessage;
+
+ const getCloseType = (): string => {
+ const types: string[] = [];
+
+ if (is_breach_close) {
+ types.push('Breach');
+ }
+ if (is_cooperative_close) {
+ types.push('Cooperative');
+ }
+ if (is_funding_cancel) {
+ types.push('Funding Cancel');
+ }
+ if (is_local_force_close) {
+ types.push('Local Force Close');
+ }
+ if (is_remote_force_close) {
+ types.push('Remote Force Close');
+ }
+
+ return types.join(', ');
+ };
+
+ toast.info(
+ renderToast(
+ 'Channel Closed',
+ <>
+ {renderLine('Reason', getCloseType())}
+ {renderLine('Capacity', formatSats(capacity))}
+ {renderLine('Id', id)}
+ {renderLine('Peer', getNodeLink(partner_public_key))}
+ {renderLine(
+ 'Tx',
+ transaction_id ? getTransactionLink(transaction_id) : null
+ )}
+ {renderLine(
+ 'Closing Tx',
+ close_transaction_id
+ ? getTransactionLink(close_transaction_id)
+ : null
+ )}
+ >
+ ),
+ options
+ );
+
+ handleRefetchQueries([
+ 'GetChannels',
+ 'GetPendingChannels',
+ 'GetClosedChannels',
+ ]);
+ }, [closed.lastMessage, handleRefetchQueries]);
+
+ useEffect(() => {
+ if (!opened.lastMessage) return;
+ const {
+ id,
+ partner_public_key,
+ remote_balance,
+ local_balance,
+ capacity,
+ is_partner_initiated,
+ is_private,
+ } = opened.lastMessage;
+
+ toast.info(
+ renderToast(
+ 'Channel Opened',
+ <>
+ {renderLine(
+ 'Initiated By',
+ is_partner_initiated ? 'Your Peer' : 'You'
+ )}
+ {renderLine('Id', id)}
+ {renderLine('Peer', getNodeLink(partner_public_key))}
+ {renderLine('Private', is_private ? 'Yes' : 'No')}
+ {renderLine('Capacity', formatSats(capacity))}
+ {renderLine('Local', formatSats(local_balance))}
+ {renderLine('Remote', formatSats(remote_balance))}
+ >
+ ),
+ options
+ );
+ handleRefetchQueries(['GetChannels', 'GetPendingChannels']);
+ }, [opened.lastMessage, handleRefetchQueries]);
+
+ useEffect(() => {
+ if (!opening.lastMessage) return;
+
+ toast.info(
+ renderToast(
+ 'Channel Opening',
+ renderLine(
+ 'Transaction',
+ getTransactionLink(opening.lastMessage.transaction_id)
+ )
+ ),
+ options
+ );
+ handleRefetchQueries(['GetChannels', 'GetPendingChannels']);
+ }, [opening.lastMessage, handleRefetchQueries]);
+};
diff --git a/src/client/src/hooks/UseNodeBalances.tsx b/src/client/src/hooks/UseNodeBalances.tsx
index b18b9df9..ba59f91c 100644
--- a/src/client/src/hooks/UseNodeBalances.tsx
+++ b/src/client/src/hooks/UseNodeBalances.tsx
@@ -11,9 +11,7 @@ const initialState = {
};
export const useNodeBalances = () => {
- const { data, loading, error } = useGetNodeBalancesQuery({
- pollInterval: 10000,
- });
+ const { data, loading, error } = useGetNodeBalancesQuery();
if (!data?.getNodeBalances || loading || error) {
return initialState;
diff --git a/src/client/src/hooks/UseSocket.tsx b/src/client/src/hooks/UseSocket.tsx
new file mode 100644
index 00000000..be28fe8b
--- /dev/null
+++ b/src/client/src/hooks/UseSocket.tsx
@@ -0,0 +1,51 @@
+import { useContext, useEffect, useRef } from 'react';
+import { Socket } from 'socket.io-client';
+import { SocketContext } from '../context/SocketContext';
+
+export const useSocket = (disabled?: boolean) => {
+ const socket = useRef(undefined);
+
+ const context = useContext(SocketContext);
+
+ if (context === undefined) {
+ throw new Error('useSocket must be used within a SocketProvider');
+ }
+
+ const { getStatus, createConnection, getError } = context;
+
+ const status = getStatus();
+ const error = getError();
+
+ useEffect(() => {
+ if (disabled) return;
+ const { socket: _socket, cleanup } = createConnection();
+ socket.current = _socket;
+ return () => {
+ cleanup();
+ };
+ }, [createConnection, disabled]);
+
+ return {
+ socket: socket.current,
+ status,
+ error,
+ };
+};
+
+export const useSocketEvent = (socket: Socket | undefined, event: string) => {
+ const context = useContext(SocketContext);
+
+ if (context === undefined) {
+ throw new Error('useSocketEvent must be used within a SocketProvider');
+ }
+
+ const { registerSharedListener, getLastMessage } = context;
+ const lastMessage = getLastMessage(event);
+ const sendMessage = (message: any) => socket?.emit(event, message);
+
+ useEffect(() => {
+ registerSharedListener(event);
+ }, [event, registerSharedListener]);
+
+ return { lastMessage, sendMessage };
+};
diff --git a/src/client/src/views/balance/AdvancedBalance.tsx b/src/client/src/views/balance/AdvancedBalance.tsx
index 985cd6da..9dc20367 100644
--- a/src/client/src/views/balance/AdvancedBalance.tsx
+++ b/src/client/src/views/balance/AdvancedBalance.tsx
@@ -28,6 +28,8 @@ import {
RebalanceSubTitle,
} from './Balance.styled';
import { PeerSelection } from './PeerSelection';
+import { LoadingCard } from '../../components/loading/LoadingCard';
+import { RebalanceLogs } from './Logs';
export type RebalanceIdType = {
alias: string;
@@ -145,6 +147,7 @@ const SettingLine: React.FC<{ title: string }> = ({ children, title }) => (
);
export const AdvancedBalance = () => {
+ const [messages, setMessages] = useState([]);
const [openType, openTypeSet] = useState('none');
const [isTarget, setIsTarget] = useState(false);
@@ -174,7 +177,7 @@ export const AdvancedBalance = () => {
}
);
- const [rebalance, { data: _data, loading }] = useBosRebalanceMutation({
+ const [rebalance, { data: _data, loading, error }] = useBosRebalanceMutation({
onError: error => toast.error(getErrorContent(error)),
onCompleted: () => {
dispatch({ type: 'clearFilters' });
@@ -311,13 +314,15 @@ export const AdvancedBalance = () => {
- {'5. Set a timeout for the rebalance attempt. (Optional)'}
+
+ {'5. Set a timeout for the rebalance attempt. (Default: 5 minutes)'}
+
{state.timeout_minutes}m : ''
}
@@ -329,9 +334,16 @@ export const AdvancedBalance = () => {
>
);
- return (
- <>
- {data && data.bosRebalance ? (
+ const renderRebalance = () => {
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+ if (data?.bosRebalance) {
+ return (
{
Balance Again
- ) : (
-
- {renderDetails()}
-
- {
- dispatch({ type: 'clearFilters' });
- }}
- >
- Reset
-
- {
- rebalance({
- variables: {
- ...(isTarget
- ? { out_inbound: state.out_inbound }
- : { max_rebalance: state.max_rebalance }),
- max_fee_rate: state.max_fee_rate,
- max_fee: state.max_fee,
- timeout_minutes: state.timeout_minutes,
- avoid: state.avoid.map(a => a.id),
- in_through: state.in_through.id,
- out_through: state.out_through.id,
- },
- });
- }}
- >
- Rebalance
-
-
-
- )}
+ );
+ }
+ };
+
+ const renderContent = () => {
+ if ((loading || data?.bosRebalance) && !error) {
+ return <>{renderRebalance()}>;
+ }
+ return (
+
+ {renderDetails()}
+
+ {
+ dispatch({ type: 'clearFilters' });
+ }}
+ >
+ Reset
+
+ {
+ setMessages([]);
+ rebalance({
+ variables: {
+ ...(isTarget
+ ? { out_inbound: state.out_inbound }
+ : { max_rebalance: state.max_rebalance }),
+ max_fee_rate: state.max_fee_rate,
+ max_fee: state.max_fee,
+ timeout_minutes: state.timeout_minutes,
+ avoid: state.avoid.map(a => a.id),
+ in_through: state.in_through.id,
+ out_through: state.out_through.id,
+ },
+ });
+ }}
+ >
+ Rebalance
+
+
+
+ );
+ };
+
+ return (
+ <>
+ {renderContent()}
+
{
diff --git a/src/client/src/views/balance/Logs.tsx b/src/client/src/views/balance/Logs.tsx
new file mode 100644
index 00000000..0c80532e
--- /dev/null
+++ b/src/client/src/views/balance/Logs.tsx
@@ -0,0 +1,83 @@
+import { FC, useEffect } from 'react';
+import { Card } from '../../components/generic/CardGeneric';
+import { renderLine } from '../../components/generic/helpers';
+import {
+ CardWithTitle,
+ SubCard,
+ SubTitle,
+} from '../../components/generic/Styled';
+import { useSocket, useSocketEvent } from '../../hooks/UseSocket';
+import { btcToSat, formatSats } from '../../utils/helpers';
+
+type RebalanceProps = {
+ messages: any[];
+ setMessages: (state: (state: any[]) => any[]) => void;
+};
+
+export const RebalanceLogs: FC = ({
+ messages,
+ setMessages,
+}) => {
+ const { socket } = useSocket();
+ const { lastMessage } = useSocketEvent(socket, 'rebalance');
+
+ useEffect(() => {
+ if (!lastMessage) return;
+ setMessages(state => [lastMessage, ...state]);
+ }, [lastMessage, setMessages]);
+
+ const renderContent = () => {
+ return messages.map(m => {
+ if ('rebalance_target_amount' in m) {
+ return (
+
+ {renderLine(
+ 'Amount',
+ formatSats(btcToSat(m.rebalance_target_amount))
+ )}
+ {renderLine('Incoming Peer', m.incoming_peer_to_decrease_inbound)}
+ {renderLine('Outgoing Peer', m.outgoing_peer_to_increase_inbound)}
+
+ );
+ }
+ if ('circular_rebalance_for' in m) {
+ return (
+
+ {renderLine('Rebalancing peer', m.circular_rebalance_for)}
+
+ );
+ }
+ if ('evaluating' in m) {
+ return (
+
+ {m.evaluating.map((s: string, index: number) =>
+ renderLine(index + 1, s)
+ )}
+
+ );
+ }
+ if ('evaluating_amount' in m) {
+ return (
+
+ {renderLine('Evaluating amount', formatSats(m.evaluating_amount))}
+
+ );
+ }
+ if ('failure' in m) {
+ return {renderLine('Failure', m.failure)};
+ }
+ return null;
+ });
+ };
+
+ if (!messages.length) {
+ return null;
+ }
+
+ return (
+
+ Logs
+ {renderContent()}
+
+ );
+};
diff --git a/src/server/app.module.ts b/src/server/app.module.ts
index 2fab85c4..a103703c 100644
--- a/src/server/app.module.ts
+++ b/src/server/app.module.ts
@@ -15,6 +15,8 @@ import { transports, format } from 'winston';
import configuration from './config/configuration';
import jwt from 'jsonwebtoken';
import cookie from 'cookie';
+import { WsModule } from './modules/ws/ws.module';
+import { SubModule } from './modules/sub/sub.module';
const { combine, timestamp, prettyPrint, json } = format;
@@ -36,6 +38,9 @@ export type JwtObjectType = {
@Module({
imports: [
+ AuthenticationModule,
+ SubModule,
+ WsModule,
ApiModule,
ViewModule,
NodeModule,
diff --git a/src/server/config/configuration.ts b/src/server/config/configuration.ts
index 096f8e66..ca399c08 100644
--- a/src/server/config/configuration.ts
+++ b/src/server/config/configuration.ts
@@ -36,6 +36,15 @@ export type YamlEnvs = {
YML_ENV_4: string;
};
+type SubscriptionsConfig = {
+ disableAll: boolean;
+ disableInvoices: boolean;
+ disablePayments: boolean;
+ disableForwards: boolean;
+ disableChannels: boolean;
+ disableBackups: boolean;
+};
+
type ConfigType = {
basePath: string;
isProduction: boolean;
@@ -53,6 +62,7 @@ type ConfigType = {
masterPasswordOverride: string;
disable2FA: boolean;
headers: Headers;
+ subscriptions: SubscriptionsConfig;
};
export default (): ConfigType => {
@@ -106,6 +116,15 @@ export default (): ConfigType => {
YML_ENV_4: process.env.YML_ENV_4 || '',
};
+ const subscriptions = {
+ disableAll: process.env.DISABLE_ALL_SUBS === 'true',
+ disableInvoices: process.env.DISABLE_INVOICE_SUB === 'true',
+ disablePayments: process.env.DISABLE_PAYMENT_SUB === 'true',
+ disableForwards: process.env.DISABLE_FORWARD_SUB === 'true',
+ disableChannels: process.env.DISABLE_CHANNEL_SUB === 'true',
+ disableBackups: process.env.DISABLE_BACKUP_SUB === 'true',
+ };
+
const config: ConfigType = {
logJson: process.env.LOG_JSON === 'true',
masterPasswordOverride: process.env.MASTER_PASSWORD_OVERRIDE || '',
@@ -123,6 +142,7 @@ export default (): ConfigType => {
urls,
jwtSecret,
yamlEnvs,
+ subscriptions,
};
if (!isProduction) {
diff --git a/src/server/modules/api/bos/bos.module.ts b/src/server/modules/api/bos/bos.module.ts
index 2f768fed..1224fd75 100644
--- a/src/server/modules/api/bos/bos.module.ts
+++ b/src/server/modules/api/bos/bos.module.ts
@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { AccountsModule } from '../../accounts/accounts.module';
+import { WsModule } from '../../ws/ws.module';
import { BosResolver } from './bos.resolver';
@Module({
- imports: [AccountsModule],
+ imports: [WsModule, AccountsModule],
providers: [BosResolver],
})
export class BosModule {}
diff --git a/src/server/modules/api/bos/bos.resolver.ts b/src/server/modules/api/bos/bos.resolver.ts
index 83bf4734..fc7676c8 100644
--- a/src/server/modules/api/bos/bos.resolver.ts
+++ b/src/server/modules/api/bos/bos.resolver.ts
@@ -11,10 +11,13 @@ import { Inject } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { to } from 'src/server/utils/async';
import { BosRebalanceResult, RebalanceResponseType } from './bos.types';
+import { WsService } from '../../ws/ws.service';
+import { stripAnsi } from 'src/server/utils/string';
@Resolver()
export class BosResolver {
constructor(
+ private wsService: WsService,
private accountsService: AccountsService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
) {}
@@ -79,7 +82,7 @@ export class BosResolver {
...(in_through && { in_through }),
...(max_fee && max_fee > 0 && { max_fee }),
...(max_fee_rate && max_fee_rate > 0 && { max_fee_rate }),
- ...(timeout_minutes && timeout_minutes > 0 && { timeout_minutes }),
+ ...(timeout_minutes ? { timeout_minutes } : { timeout_minutes: 5 }),
...(max_rebalance && max_rebalance > 0
? { max_rebalance: `${max_rebalance}` }
: {}),
@@ -92,10 +95,33 @@ export class BosResolver {
this.logger.info('Rebalance Params', { filteredParams });
+ const logger = {
+ info: (message, ...args) => {
+ let payload = message;
+
+ if (payload?.evaluating?.length) {
+ payload = {
+ evaluating: payload.evaluating.map((m: string) => stripAnsi(m)),
+ };
+ }
+
+ this.wsService.emit(user.id, 'rebalance', payload);
+ this.logger.info(message, args);
+ },
+ warn: (message, ...args) => {
+ this.wsService.emit(user.id, 'rebalance', message);
+ this.logger.warn(message, args);
+ },
+ error: (message, ...args) => {
+ this.wsService.emit(user.id, 'rebalance', message);
+ this.logger.error(message, args);
+ },
+ };
+
const response = await to(
rebalance({
lnd: account.lnd,
- logger: this.logger,
+ logger,
fs: { getFile: fs.readFile },
...filteredParams,
})
diff --git a/src/server/modules/auth/auth.module.ts b/src/server/modules/auth/auth.module.ts
new file mode 100644
index 00000000..5a363f96
--- /dev/null
+++ b/src/server/modules/auth/auth.module.ts
@@ -0,0 +1,20 @@
+import { Module } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { JwtModule } from '@nestjs/jwt';
+import { AccountsModule } from '../accounts/accounts.module';
+import { AuthenticationService } from './auth.service';
+
+@Module({
+ imports: [
+ AccountsModule,
+ JwtModule.registerAsync({
+ inject: [ConfigService],
+ useFactory: (config: ConfigService) => ({
+ secret: config.get('jwtSecret'),
+ }),
+ }),
+ ],
+ providers: [AuthenticationService],
+ exports: [AuthenticationService],
+})
+export class AuthenticationModule {}
diff --git a/src/server/modules/auth/auth.service.ts b/src/server/modules/auth/auth.service.ts
new file mode 100644
index 00000000..39597258
--- /dev/null
+++ b/src/server/modules/auth/auth.service.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@nestjs/common';
+import { JwtService } from '@nestjs/jwt';
+import { AccountsService } from '../accounts/accounts.service';
+
+@Injectable()
+export class AuthenticationService {
+ constructor(
+ private readonly jwtService: JwtService,
+ private accountsService: AccountsService
+ ) {}
+
+ public async getUserFromAuthToken(token: string) {
+ const payload = this.jwtService.verify(token);
+
+ if (payload.sub) {
+ const account = this.accountsService.getAccount(payload.sub);
+
+ if (account) {
+ return payload.sub;
+ }
+ }
+ }
+}
diff --git a/src/server/modules/node/lnd/lnd.helpers.ts b/src/server/modules/node/lnd/lnd.helpers.ts
index 320845a7..f325724a 100644
--- a/src/server/modules/node/lnd/lnd.helpers.ts
+++ b/src/server/modules/node/lnd/lnd.helpers.ts
@@ -24,6 +24,10 @@ export const getErrorMsg = (error: any[] | string): string => {
return errorString;
}
+ if (error[1] && typeof error[1] === 'string') {
+ return error[1];
+ }
+
console.log('Unknown Error:', error);
return 'Unknown Error';
};
diff --git a/src/server/modules/sub/sub.module.ts b/src/server/modules/sub/sub.module.ts
new file mode 100644
index 00000000..1f83225b
--- /dev/null
+++ b/src/server/modules/sub/sub.module.ts
@@ -0,0 +1,10 @@
+import { Module } from '@nestjs/common';
+import { AccountsModule } from '../accounts/accounts.module';
+import { WsModule } from '../ws/ws.module';
+import { SubService } from './sub.service';
+
+@Module({
+ imports: [AccountsModule, WsModule],
+ providers: [SubService],
+})
+export class SubModule {}
diff --git a/src/server/modules/sub/sub.service.ts b/src/server/modules/sub/sub.service.ts
new file mode 100644
index 00000000..56f9c599
--- /dev/null
+++ b/src/server/modules/sub/sub.service.ts
@@ -0,0 +1,433 @@
+import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common';
+import {
+ subscribeToForwards,
+ getWalletInfo,
+ subscribeToChannels,
+ subscribeToInvoices,
+ subscribeToBackups,
+ subscribeToPastPayments,
+} from 'ln-service';
+import asyncAuto from 'async/auto';
+import asyncEach from 'async/each';
+import asyncMap from 'async/map';
+import asyncForever from 'async/forever';
+import { Logger } from 'winston';
+import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
+import { AccountsService } from '../accounts/accounts.service';
+import { WsService } from '../ws/ws.service';
+import { ConfigService } from '@nestjs/config';
+
+const restartSubscriptionTimeMs = 1000 * 30;
+
+type NodeType = {
+ id: string;
+ name: string;
+ pubkey: string;
+ lnd: any;
+};
+
+@Injectable()
+export class SubService implements OnApplicationBootstrap {
+ subscriptions = [];
+
+ constructor(
+ private accountsService: AccountsService,
+ private wsService: WsService,
+ private configService: ConfigService,
+ @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
+ ) {}
+
+ async onApplicationBootstrap(): Promise {
+ const disabled = this.configService.get('subscriptions.disableAll');
+ if (disabled) {
+ this.logger.info('All subscriptions are disabled');
+ return;
+ }
+
+ this.startSubscription();
+ }
+
+ startSubscription() {
+ return asyncForever(
+ next => {
+ return asyncAuto(
+ {
+ // Get Authenticated LND objects for each node
+ getNodes: callback => {
+ const accounts = this.accountsService.getAllAccounts();
+
+ const validAccounts = [];
+
+ for (const key in accounts) {
+ if (accounts.hasOwnProperty(key)) {
+ const account = accounts[key];
+ if (!account.encrypted) {
+ validAccounts.push({ id: account.hash, lnd: account.lnd });
+ }
+ }
+ }
+
+ callback(null, validAccounts);
+ },
+
+ // Try to connect to nodes
+ checkNodes: [
+ 'getNodes',
+ async ({ getNodes }) => {
+ return asyncMap(getNodes, async ({ lnd, id }) => {
+ try {
+ const info = await getWalletInfo({ lnd });
+
+ const sliced = info.public_key.slice(0, 10);
+ const name = `${info.alias}(${sliced})`;
+
+ return {
+ id,
+ name,
+ pubkey: info.public_key,
+ lnd,
+ };
+ } catch (err) {
+ this.logger.error('Error connecting to node', {
+ id,
+ err,
+ });
+ }
+ });
+ },
+ ],
+
+ // Check which nodes are available and remove duplicates
+ checkAvailable: [
+ 'checkNodes',
+ async ({ checkNodes }: { checkNodes: NodeType[] }) => {
+ const unique = checkNodes.filter(Boolean);
+
+ if (!unique.length) {
+ this.logger.error(`Unable to connect to any node.`);
+
+ throw new Error('UnableToConnectToAnyNode');
+ }
+
+ const names = unique.map(a => a.name);
+
+ this.logger.info(`Connected to ${names.join(', ')}`);
+
+ return unique;
+ },
+ ],
+
+ // Subscribe to node invoices
+ invoices: [
+ 'checkAvailable',
+ async ({ checkAvailable }, callback) => {
+ const disabled = this.configService.get(
+ 'subscriptions.disableInvoices'
+ );
+ if (disabled) {
+ this.logger.info('Invoice subscriptions are disabled');
+ return;
+ }
+
+ const names = checkAvailable.map(a => a.name);
+
+ this.logger.info('Invoice subscription', {
+ connections: names.join(', '),
+ });
+
+ return asyncEach(
+ checkAvailable,
+ (node, cbk) => {
+ const sub = subscribeToInvoices({ lnd: node.lnd });
+
+ this.subscriptions.push(sub);
+
+ sub.on('invoice_updated', data => {
+ this.logger.info('invoice_updated', { node: node.name });
+ this.wsService.emit(node.id, 'invoice_updated', data);
+
+ return;
+ });
+
+ sub.on('error', async err => {
+ sub.removeAllListeners();
+
+ this.logger.error(
+ `ErrorInInvoiceSubscribe: ${node.name}`,
+ { err }
+ );
+
+ cbk([
+ 'ErrorInInvoiceSubscribe',
+ { node: node.name, err },
+ ]);
+ });
+ },
+ callback
+ );
+ },
+ ],
+
+ // Subscribe to node payments
+ payments: [
+ 'checkAvailable',
+ async ({ checkAvailable }, callback) => {
+ const disabled = this.configService.get(
+ 'subscriptions.disablePayments'
+ );
+ if (disabled) {
+ this.logger.info('Payment subscriptions are disabled');
+ return;
+ }
+
+ const names = checkAvailable.map(a => a.name);
+
+ this.logger.info('Payment subscription', {
+ connections: names.join(', '),
+ });
+
+ return asyncEach(
+ checkAvailable,
+ (node, cbk) => {
+ const sub = subscribeToPastPayments({ lnd: node.lnd });
+
+ this.subscriptions.push(sub);
+
+ sub.on('payment', data => {
+ this.logger.info('payment', { node: node.name });
+ this.wsService.emit(node.id, 'payment', data);
+
+ return;
+ });
+
+ sub.on('error', async err => {
+ sub.removeAllListeners();
+
+ this.logger.error(
+ `ErrorInPaymentSubscribe: ${node.name}`,
+ { err }
+ );
+
+ cbk([
+ 'ErrorInPaymentSubscribe',
+ { node: node.name, err },
+ ]);
+ });
+ },
+ callback
+ );
+ },
+ ],
+
+ // Subscribe to node forwards
+ forwards: [
+ 'checkAvailable',
+ async ({ checkAvailable }, callback) => {
+ const disabled = this.configService.get(
+ 'subscriptions.disableForwards'
+ );
+ if (disabled) {
+ this.logger.info('Forward subscriptions are disabled');
+ return;
+ }
+
+ const names = checkAvailable.map(a => a.name);
+
+ this.logger.info('Forward subscription', {
+ connections: names.join(', '),
+ });
+
+ return asyncEach(
+ checkAvailable,
+ (node, cbk) => {
+ const sub = subscribeToForwards({ lnd: node.lnd });
+
+ this.subscriptions.push(sub);
+
+ sub.on('forward', data => {
+ this.logger.info('forward', { node: node.name });
+ this.wsService.emit(node.id, 'forward', data);
+
+ return;
+ });
+
+ sub.on('error', async err => {
+ sub.removeAllListeners();
+
+ this.logger.error(
+ `ErrorInForwardSubscribe: ${node.name}`,
+ { err }
+ );
+
+ cbk([
+ 'ErrorInForwardSubscribe',
+ { node: node.name, err },
+ ]);
+ });
+ },
+ callback
+ );
+ },
+ ],
+
+ // Subscribe to node channels
+ channels: [
+ 'checkAvailable',
+ async ({ checkAvailable }, callback) => {
+ const disabled = this.configService.get(
+ 'subscriptions.disableChannels'
+ );
+ if (disabled) {
+ this.logger.info('Channel subscriptions are disabled');
+ return;
+ }
+
+ const names = checkAvailable.map(a => a.name);
+
+ this.logger.info('Channels subscription', {
+ connections: names.join(', '),
+ });
+
+ return asyncEach(
+ checkAvailable,
+ (node, cbk) => {
+ const sub = subscribeToChannels({ lnd: node.lnd });
+
+ this.subscriptions.push(sub);
+
+ sub.on('channel_active_changed', data => {
+ this.logger.info('channel_active_changed', {
+ node: node.name,
+ });
+ this.wsService.emit(
+ node.id,
+ 'channel_active_changed',
+ data
+ );
+
+ return;
+ });
+
+ sub.on('channel_closed', data => {
+ this.logger.info('channel_closed', { node: node.name });
+ this.wsService.emit(node.id, 'channel_closed', data);
+
+ return;
+ });
+
+ sub.on('channel_opened', data => {
+ this.logger.info('channel_opened', { node: node.name });
+ this.wsService.emit(node.id, 'channel_opened', data);
+
+ return;
+ });
+
+ sub.on('channel_opening', data => {
+ this.logger.info('channel_opening', { node: node.name });
+ this.wsService.emit(node.id, 'channel_opening', data);
+
+ return;
+ });
+
+ sub.on('error', async err => {
+ sub.removeAllListeners();
+
+ this.logger.error(
+ `ErrorInChannelSubscribe: ${node.name}`,
+ { err }
+ );
+
+ cbk([
+ 'ErrorInChannelSubscribe',
+ { node: node.name, err },
+ ]);
+ });
+ },
+ callback
+ );
+ },
+ ],
+
+ // // Subscribe to node backups
+ backups: [
+ 'checkAvailable',
+ ({ checkAvailable }, callback) => {
+ const disabled = this.configService.get(
+ 'subscriptions.disableBackups'
+ );
+ if (disabled) {
+ this.logger.info('Backup subscriptions are disabled');
+ return;
+ }
+
+ const names = checkAvailable.map(a => a.name);
+
+ this.logger.info('Backup subscription', {
+ connections: names.join(', '),
+ });
+
+ return asyncEach(
+ checkAvailable,
+ (node, cbk) => {
+ let postBackupTimeoutHandle;
+ const sub = subscribeToBackups({ lnd: node.lnd });
+
+ this.subscriptions.push(sub);
+
+ sub.on('backup', ({ backup }) => {
+ if (!!postBackupTimeoutHandle) {
+ clearTimeout(postBackupTimeoutHandle);
+ }
+
+ postBackupTimeoutHandle = setTimeout(async () => {
+ const time = Math.round(new Date().getTime() / 1000);
+ this.logger.info('channel_backup', { node: node.name });
+ this.wsService.emit(node.id, 'channel_backup', {
+ file: `${time}-${node.name}-backup.txt`,
+ backup,
+ });
+ }, restartSubscriptionTimeMs);
+
+ return;
+ });
+
+ sub.on('error', async err => {
+ sub.removeAllListeners();
+
+ this.logger.error(`ErrorInBackupSubscribe: ${node.name}`);
+ cbk(['ErrorInBackupSubscribe', { node: node.name, err }]);
+ });
+ },
+ callback
+ );
+ },
+ ],
+ },
+ async (err, results) => {
+ this.subscriptions.forEach(sub => sub.removeAllListeners());
+ this.subscriptions = [];
+ this.logger.error(err?.message || '.....');
+ if (err?.message === 'UnableToConnectToAnyNode') {
+ next('UnableToConnectToAnyNode');
+ return;
+ }
+ if (err) {
+ this.logger.error('AsyncAuto error:', err);
+ } else {
+ this.logger.error('AsyncAuto results:', results);
+ }
+ const message = `Restarting subscription after ${restartSubscriptionTimeMs} ms`;
+ this.logger.warn(message);
+ setTimeout(async () => {
+ this.logger.warn('Restarting...');
+ next(null, 'retry');
+ }, restartSubscriptionTimeMs);
+ }
+ );
+ },
+ async err => {
+ this.logger.error('Initiating subscriptions failed: ', err);
+ }
+ );
+ }
+}
diff --git a/src/server/modules/ws/ws.gateway.ts b/src/server/modules/ws/ws.gateway.ts
new file mode 100644
index 00000000..cacb4f5e
--- /dev/null
+++ b/src/server/modules/ws/ws.gateway.ts
@@ -0,0 +1,68 @@
+import {
+ WebSocketGateway,
+ OnGatewayInit,
+ OnGatewayConnection,
+ OnGatewayDisconnect,
+ WebSocketServer,
+} from '@nestjs/websockets';
+import { Inject } from '@nestjs/common';
+import { Server, Socket } from 'socket.io';
+import { WsService } from './ws.service';
+import { parse } from 'cookie';
+import { appConstants } from 'src/server/utils/appConstants';
+import { AuthenticationService } from '../auth/auth.service';
+import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
+import { Logger } from 'winston';
+import { ConfigModule } from '@nestjs/config';
+
+ConfigModule.forRoot({
+ envFilePath: ['.env.local', '.env'],
+});
+
+@WebSocketGateway({ path: `${process.env.BASE_PATH || ''}/socket.io` })
+export class WsGateway
+ implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
+{
+ constructor(
+ private socketService: WsService,
+ private authService: AuthenticationService,
+ @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
+ ) {}
+
+ @WebSocketServer()
+ public server: Server;
+
+ async getUserFromSocket(socket: Socket) {
+ const cookie = parse(socket.handshake.headers.cookie);
+
+ const authToken = cookie[appConstants.cookieName] || '';
+ if (!authToken) return null;
+
+ const user = await this.authService.getUserFromAuthToken(authToken);
+ if (!user) return null;
+
+ return user;
+ }
+
+ afterInit(server: Server) {
+ this.logger.info('WS server created');
+ this.socketService.init(server);
+ }
+
+ async handleDisconnect(client: Socket) {
+ const user = await this.getUserFromSocket(client);
+ client.leave(user);
+ this.logger.info(`Client disconnected: ${client.id}`);
+ }
+
+ async handleConnection(client: Socket) {
+ const user = await this.getUserFromSocket(client);
+
+ if (!user) {
+ client.disconnect();
+ } else {
+ client.join(user);
+ this.logger.info(`Client connected: ${client.id}`);
+ }
+ }
+}
diff --git a/src/server/modules/ws/ws.module.ts b/src/server/modules/ws/ws.module.ts
new file mode 100644
index 00000000..3df15332
--- /dev/null
+++ b/src/server/modules/ws/ws.module.ts
@@ -0,0 +1,11 @@
+import { Module } from '@nestjs/common';
+import { AuthenticationModule } from '../auth/auth.module';
+import { WsGateway } from './ws.gateway';
+import { WsService } from './ws.service';
+
+@Module({
+ imports: [AuthenticationModule],
+ providers: [WsGateway, WsService],
+ exports: [WsService],
+})
+export class WsModule {}
diff --git a/src/server/modules/ws/ws.service.ts b/src/server/modules/ws/ws.service.ts
new file mode 100644
index 00000000..d217a42d
--- /dev/null
+++ b/src/server/modules/ws/ws.service.ts
@@ -0,0 +1,16 @@
+import { Injectable } from '@nestjs/common';
+import { Server } from 'socket.io';
+
+@Injectable()
+export class WsService {
+ private socket: Server = null;
+
+ init(socket: Server) {
+ this.socket = socket;
+ }
+
+ emit(account: string, event: string, payload: any) {
+ if (!this.socket || !account || !event || !payload) return;
+ this.socket.in(account).emit(event, payload);
+ }
+}
diff --git a/src/server/utils/string.ts b/src/server/utils/string.ts
index ee008e17..6c1825e8 100644
--- a/src/server/utils/string.ts
+++ b/src/server/utils/string.ts
@@ -9,3 +9,20 @@ export const shorten = (text: string): string => {
export const reversedBytes = hex =>
Buffer.from(hex, 'hex').reverse().toString('hex');
+
+const ansiRegex = ({ onlyFirst = false } = {}) => {
+ const pattern = [
+ '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
+ '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
+ ].join('|');
+
+ return new RegExp(pattern, onlyFirst ? undefined : 'g');
+};
+
+export const stripAnsi = string => {
+ if (typeof string !== 'string') {
+ throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
+ }
+
+ return string.replace(ansiRegex(), '');
+};