diff --git a/lib/BaseClient.ts b/lib/BaseClient.ts index ae65eb72..2b518f08 100644 --- a/lib/BaseClient.ts +++ b/lib/BaseClient.ts @@ -4,7 +4,7 @@ import { ClientStatus } from './consts/Enums'; class BaseClient extends EventEmitter { protected status = ClientStatus.Disconnected; - protected readonly RECONNECT_INTERVAL = 1000; + protected readonly RECONNECT_INTERVAL = 5000; protected reconnectionTimer?: any; constructor() { diff --git a/lib/lightning/LndClient.ts b/lib/lightning/LndClient.ts index 15fb2f25..f085fe8f 100644 --- a/lib/lightning/LndClient.ts +++ b/lib/lightning/LndClient.ts @@ -20,24 +20,6 @@ type LndConfig = { macaroonpath: string; }; -/** - * General information about the state of this LND client - */ -type Info = { - version: string; - syncedtochain: boolean; - chainsList: string[]; - channels: ChannelCount; - blockheight: number; - uris?: string[]; -}; - -type ChannelCount = { - active: number; - pending: number; - inactive?: number; -}; - type SendResponse = { paymentPreimage: Buffer; paymentHash: Uint8Array | string; @@ -65,6 +47,12 @@ interface LndClient { on(event: 'channel.backup', listener: (channelBackup: string) => void): this; emit(event: 'channel.backup', channelBackup: string): boolean; + + on(event: 'subscription.error', listener: () => void): this; + emit(event: 'subscription.error'): this; + + on(event: 'subscription.reconnected', listener: () => void): this; + emit(event: 'subscription.reconnected'): this; } /** @@ -137,12 +125,12 @@ class LndClient extends BaseClient implements LndClient { this.clearReconnectTimer(); this.setClientStatus(ClientStatus.Connected); - - return true; } catch (error) { this.setClientStatus(ClientStatus.Disconnected); - this.logger.error(`Could not connect to ${this.symbol} ${LndClient.serviceName} at ${this.uri}` + - ` because: "${error.details}", retrying in ${this.RECONNECT_INTERVAL} ms`); + + this.logger.error(`Could not connect to ${LndClient.serviceName} ${this.symbol} at ${this.uri}: ${formatError(error)}`); + this.logger.info(`Retrying in ${this.RECONNECT_INTERVAL} ms`); + this.reconnectionTimer = setTimeout(this.connect, this.RECONNECT_INTERVAL); return false; @@ -153,22 +141,27 @@ class LndClient extends BaseClient implements LndClient { } private reconnect = async () => { + this.setClientStatus(ClientStatus.Disconnected); + try { await this.getInfo(); this.logger.info(`Reestablished connection to ${LndClient.serviceName} ${this.symbol}`); - this.setClientStatus(ClientStatus.Connected); this.clearReconnectTimer(); this.subscribePeerEvents(); this.subscribeChannelEvents(); this.subscribeChannelBackups(); + + this.setClientStatus(ClientStatus.Connected); + this.emit('subscription.reconnected'); } catch (err) { + this.setClientStatus(ClientStatus.Disconnected); + this.logger.error(`Could not reconnect to ${LndClient.serviceName} ${this.symbol}: ${err}`); this.logger.info(`Retrying in ${this.RECONNECT_INTERVAL} ms`); - this.setClientStatus(ClientStatus.Disconnected); this.reconnectionTimer = setTimeout(this.reconnect, this.RECONNECT_INTERVAL); } } @@ -522,11 +515,20 @@ class LndClient extends BaseClient implements LndClient { }) .on('end', () => deleteSubscription()) .on('error', (error) => { - this.logger.error(`Invoice subscription errored: ${error.message}`); + this.logger.error(`${LndClient.serviceName} ${this.symbol} invoice subscription errored: ${error.message}`); deleteSubscription(); }); } + private handleSubscriptionError = async (subscriptionName: string, error: any) => { + this.logger.error(`${LndClient.serviceName} ${this.symbol} ${subscriptionName} subscription errored: ${formatError(error)}`); + + if (this.status === ClientStatus.Connected) { + this.emit('subscription.error'); + await this.reconnect(); + } + } + private subscribePeerEvents = () => { if (this.peerEventSubscription) { this.peerEventSubscription.cancel(); @@ -539,8 +541,7 @@ class LndClient extends BaseClient implements LndClient { } }) .on('error', async (error) => { - this.logger.error(`Peer event subscription errored: ${formatError(error)}`); - await this.reconnect(); + await this.handleSubscriptionError('peer event', error); }); } @@ -556,8 +557,7 @@ class LndClient extends BaseClient implements LndClient { } }) .on('error', async(error) => { - this.logger.error(`Channel event subscription errored: ${formatError(error)}`); - await this.reconnect(); + await this.handleSubscriptionError('channel event', error); }); } @@ -576,11 +576,10 @@ class LndClient extends BaseClient implements LndClient { } }) .on('error', async (error) => { - this.logger.error(`Channel backup subscription errored: ${formatError(error)}`); - await this.reconnect(); + await this.handleSubscriptionError('channel backup', error); }); } } export default LndClient; -export { LndConfig, SendResponse, Info }; +export { LndConfig, SendResponse }; diff --git a/lib/notifications/NotificationProvider.ts b/lib/notifications/NotificationProvider.ts index f5ad3aa4..1341a2ea 100644 --- a/lib/notifications/NotificationProvider.ts +++ b/lib/notifications/NotificationProvider.ts @@ -72,6 +72,13 @@ class NotificationProvider { await this.discord.sendMessage('Started Boltz instance'); this.logger.verbose('Connected to Discord'); + for (const [, currency] of this.service.currencies) { + if (currency.lndClient) { + currency.lndClient.on('subscription.error', async () => await this.sendLostConnection(`LND ${currency.symbol}`)); + currency.lndClient.on('subscription.reconnected', async () => await this.sendReconnected(`LND ${currency.symbol}`)); + } + } + const check = async () => { await Promise.all([ this.checkBalances(), @@ -102,7 +109,7 @@ class NotificationProvider { const promises: Promise[] = []; info.getChainsMap().forEach((currency: CurrencyInfo, symbol: string) => { - promises.push(this.checkConnection(`${symbol} LND`, currency.getLnd())); + promises.push(this.checkConnection(`LND ${symbol}`, currency.getLnd())); promises.push(this.checkConnection(`${symbol} node`, currency.getChain())); }); @@ -112,19 +119,13 @@ class NotificationProvider { private checkConnection = async (service: string, object: ChainInfo | LndInfo | undefined) => { if (object !== undefined) { if (object.getError() === '') { - if (this.disconnected.has(service)) { - this.disconnected.delete(service); - await this.sendReconnected(service); - } + await this.sendReconnected(service); return; } } - if (!this.disconnected.has(service)) { - this.disconnected.add(service); - await this.sendLostConnection(service); - } + await this.sendLostConnection(service); } private checkBalances = async () => { @@ -301,11 +302,17 @@ class NotificationProvider { } private sendLostConnection = async (service: string) => { - await this.discord.sendMessage(`**Lost connection to ${service}**`); + if (!this.disconnected.has(service)) { + this.disconnected.add(service); + await this.discord.sendMessage(`**Lost connection to ${service}**`); + } } private sendReconnected = async (service: string) => { - await this.discord.sendMessage(`Reconnected to ${service}`); + if (this.disconnected.has(service)) { + this.disconnected.delete(service); + await this.discord.sendMessage(`Reconnected to ${service}`); + } } private formatBalances = (balance: number, threshold: number) => { diff --git a/lib/service/Service.ts b/lib/service/Service.ts index dc437fb0..09f6d0bf 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -71,7 +71,7 @@ class Service { private logger: Logger, config: ConfigType, private walletManager: WalletManager, - private currencies: Map, + public currencies: Map, ) { this.prepayMinerFee = config.prepayminerfee; this.logger.debug(`Prepay miner fee for Reverse Swaps is ${this.prepayMinerFee ? 'enabled' : 'disabled' }`); diff --git a/package-lock.json b/package-lock.json index 8c9a2ed4..7ee3411b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1986,12 +1986,12 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.9.0.tgz", - "integrity": "sha512-UD6b4p0/hSe1xdTvRCENSx7iQ+KR6ourlZFfYuPC7FlXEzdHuLPrEmuxZ23b2zW96KJX9Z3w05GE/wNOiEzrVg==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.9.1.tgz", + "integrity": "sha512-XIr+Mfv7i4paEdBf0JFdIl9/tVxyj+rlilWIfZ97Be0lZ7hPvUbS5iHt9Glc8kRI53dsr0PcAEudbf8rO2wGgg==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "3.9.0", + "@typescript-eslint/experimental-utils": "3.9.1", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -2008,45 +2008,45 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.9.0.tgz", - "integrity": "sha512-/vSHUDYizSOhrOJdjYxPNGfb4a3ibO8zd4nUKo/QBFOmxosT3cVUV7KIg8Dwi6TXlr667G7YPqFK9+VSZOorNA==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.9.1.tgz", + "integrity": "sha512-lkiZ8iBBaYoyEKhCkkw4SAeatXyBq9Ece5bZXdLe1LWBUwTszGbmbiqmQbwWA8cSYDnjWXp9eDbXpf9Sn0hLAg==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.9.0", - "@typescript-eslint/typescript-estree": "3.9.0", + "@typescript-eslint/types": "3.9.1", + "@typescript-eslint/typescript-estree": "3.9.1", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.9.0.tgz", - "integrity": "sha512-rDHOKb6uW2jZkHQniUQVZkixQrfsZGUCNWWbKWep4A5hGhN5dLHMUCNAWnC4tXRlHedXkTDptIpxs6e4Pz8UfA==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.9.1.tgz", + "integrity": "sha512-y5QvPFUn4Vl4qM40lI+pNWhTcOWtpZAJ8pOEQ21fTTW4xTJkRplMjMRje7LYTXqVKKX9GJhcyweMz2+W1J5bMg==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "3.9.0", - "@typescript-eslint/types": "3.9.0", - "@typescript-eslint/typescript-estree": "3.9.0", + "@typescript-eslint/experimental-utils": "3.9.1", + "@typescript-eslint/types": "3.9.1", + "@typescript-eslint/typescript-estree": "3.9.1", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/types": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.9.0.tgz", - "integrity": "sha512-rb6LDr+dk9RVVXO/NJE8dT1pGlso3voNdEIN8ugm4CWM5w5GimbThCMiMl4da1t5u3YwPWEwOnKAULCZgBtBHg==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.9.1.tgz", + "integrity": "sha512-15JcTlNQE1BsYy5NBhctnEhEoctjXOjOK+Q+rk8ugC+WXU9rAcS2BYhoh6X4rOaXJEpIYDl+p7ix+A5U0BqPTw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.9.0.tgz", - "integrity": "sha512-N+158NKgN4rOmWVfvKOMoMFV5n8XxAliaKkArm/sOypzQ0bUL8MSnOEBW3VFIeffb/K5ce/cAV0yYhR7U4ALAA==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.9.1.tgz", + "integrity": "sha512-IqM0gfGxOmIKPhiHW/iyAEXwSVqMmR2wJ9uXHNdFpqVvPaQ3dWg302vW127sBpAiqM9SfHhyS40NKLsoMpN2KA==", "dev": true, "requires": { - "@typescript-eslint/types": "3.9.0", - "@typescript-eslint/visitor-keys": "3.9.0", + "@typescript-eslint/types": "3.9.1", + "@typescript-eslint/visitor-keys": "3.9.1", "debug": "^4.1.1", "glob": "^7.1.6", "is-glob": "^4.0.1", @@ -2064,9 +2064,9 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.9.0.tgz", - "integrity": "sha512-O1qeoGqDbu0EZUC/MZ6F1WHTIzcBVhGqDj3LhTnj65WUA548RXVxUHbYhAW9bZWfb2rnX9QsbbP5nmeJ5Z4+ng==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.9.1.tgz", + "integrity": "sha512-zxdtUjeoSh+prCpogswMwVUJfEFmCOjdzK9rpNjNBfm6EyPt99x3RrJoBOGZO23FCt0WPKUCOL5mb/9D5LjdwQ==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" @@ -14539,9 +14539,9 @@ "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, "truffle": { - "version": "5.1.40", - "resolved": "https://registry.npmjs.org/truffle/-/truffle-5.1.40.tgz", - "integrity": "sha512-frlNrytZLxy3PwN6SZ7oL7wmXr+tyNw3oDq3GwAwI7EM/koRwIyCNvSxnwTBtzYyC7Z4D0BOGQUnCh/Tuv451Q==", + "version": "5.1.41", + "resolved": "https://registry.npmjs.org/truffle/-/truffle-5.1.41.tgz", + "integrity": "sha512-6vphA82Os7HvrzqkMy0o2kxP0SYsf7glHE8U8jk15lbUNOy76SrBLmTi7at7xFkIq6LMgv03YRf0EFEN/qwAxg==", "dev": true, "requires": { "app-module-path": "^2.2.0", diff --git a/package.json b/package.json index 1eddbfaa..244ab463 100644 --- a/package.json +++ b/package.json @@ -94,8 +94,8 @@ "@types/web3": "^1.2.2", "@types/yargs": "^15.0.5", "@types/zeromq": "^4.6.3", - "@typescript-eslint/eslint-plugin": "^3.9.0", - "@typescript-eslint/parser": "^3.9.0", + "@typescript-eslint/eslint-plugin": "^3.9.1", + "@typescript-eslint/parser": "^3.9.1", "concurrently": "^5.3.0", "conventional-changelog": "^3.1.23", "conventional-changelog-cli": "^2.1.0", @@ -107,7 +107,7 @@ "grpc-tools": "^1.9.1", "grpc_tools_node_protoc_ts": "^4.1.3", "jest": "26.4.0", - "truffle": "^5.1.40", + "truffle": "^5.1.41", "ts-jest": "26.2.0", "ts-node": "8.10.2", "ts-protoc-gen": "^0.12.0", diff --git a/test/unit/notifications/NotificationProvider.spec.ts b/test/unit/notifications/NotificationProvider.spec.ts index 978f1ef6..3a64eb8b 100644 --- a/test/unit/notifications/NotificationProvider.spec.ts +++ b/test/unit/notifications/NotificationProvider.spec.ts @@ -41,6 +41,7 @@ jest.mock('../../../lib/service/Service', () => { } }, }, + currencies: new Map(), getInfo: mockGetInfo, getBalance: mockGetBalance, };