diff --git a/app/lib/lnd/neutrino.js b/app/lib/lnd/neutrino.js index a516f9e6..ea96aecf 100644 --- a/app/lib/lnd/neutrino.js +++ b/app/lib/lnd/neutrino.js @@ -1,8 +1,11 @@ +// @flow + import split2 from 'split2' import { spawn } from 'child_process' import EventEmitter from 'events' import { mainLog, lndLog, lndLogGetLevel } from '../utils/log' import { fetchBlockHeight } from './util' +import LndConfig from './config' // Sync statuses const CHAIN_SYNC_PENDING = 'chain-sync-pending' @@ -24,7 +27,16 @@ const GOT_LND_CFILTER_HEIGHT = 'got-lnd-cfilter-height' * @extends EventEmitter */ class Neutrino extends EventEmitter { - constructor(lndConfig) { + lndConfig: LndConfig + process: any + walletUnlockerGrpcActive: boolean + lightningGrpcActive: boolean + chainSyncStatus: string + currentBlockHeight: number + lndBlockHeight: number + lndCfilterHeight: number + + constructor(lndConfig: LndConfig) { super() this.lndConfig = lndConfig this.process = null @@ -36,6 +48,15 @@ class Neutrino extends EventEmitter { this.lndCfilterHeight = 0 } + static incrementIfHigher = (context: any, property: string, newVal: any): boolean => { + const { [property]: oldVal } = context + if (newVal > oldVal) { + context[property] = newVal + return true + } + return false + } + /** * Start the Lnd process in Neutrino mode. * @return {Number} PID of the Lnd process that was started. @@ -169,18 +190,18 @@ class Neutrino extends EventEmitter { if (!this.lndCfilterHeight || this.lndCfilterHeight > this.currentBlockHeight - 10000) { if ((match = line.match(/Fetching filter for height=(\d+)/))) { - cfilter = Number(match[1]) + cfilter = match[1] } } if (height) { this.setState(CHAIN_SYNC_IN_PROGRESS) - this.setLndBlockHeight(height) + this.setLndBlockHeight(Number(height)) } if (cfilter) { this.setState(CHAIN_SYNC_IN_PROGRESS) - this.setLndCfilterHeight(cfilter) + this.setLndCfilterHeight(Number(cfilter)) } // Lnd syncing has completed. @@ -208,7 +229,7 @@ class Neutrino extends EventEmitter { * @param {String} state State to compare against the current state. * @return {Boolean} Boolean indicating if the current state matches the passed in state. */ - is(state) { + is(state: string) { return this.chainSyncStatus === state } @@ -216,7 +237,7 @@ class Neutrino extends EventEmitter { * Set the current state and emit an event to notify others if te state as canged. * @param {String} state Target state. */ - setState(state) { + setState(state: string) { if (state !== this.chainSyncStatus) { this.chainSyncStatus = state this.emit(state) @@ -227,11 +248,10 @@ class Neutrino extends EventEmitter { * Set the current block height and emit an event to notify others if it has changed. * @param {String|Number} height Block height */ - setCurrentBlockHeight(height) { - const heightAsNumber = Number(height) - if (heightAsNumber > this.currentBlockHeight) { - this.currentBlockHeight = heightAsNumber - this.emit(GOT_CURRENT_BLOCK_HEIGHT, heightAsNumber) + setCurrentBlockHeight(height: number) { + const changed = Neutrino.incrementIfHigher(this, 'currentBlockHeight', height) + if (changed) { + this.emit(GOT_CURRENT_BLOCK_HEIGHT, height) } } @@ -239,14 +259,11 @@ class Neutrino extends EventEmitter { * Set the lnd block height and emit an event to notify others if it has changed. * @param {String|Number} height Block height */ - setLndBlockHeight(height) { - const heightAsNumber = Number(height) - if (heightAsNumber > this.lndBlockHeight) { - this.lndBlockHeight = heightAsNumber - this.emit(GOT_LND_BLOCK_HEIGHT, heightAsNumber) - } - if (heightAsNumber > this.currentBlockHeight) { - this.setCurrentBlockHeight(heightAsNumber) + setLndBlockHeight(height: number) { + const changed = Neutrino.incrementIfHigher(this, 'lndBlockHeight', height) + if (changed) { + this.emit(GOT_LND_BLOCK_HEIGHT, height) + this.setCurrentBlockHeight(height) } } @@ -254,7 +271,7 @@ class Neutrino extends EventEmitter { * Set the lnd cfilter height and emit an event to notify others if it has changed. * @param {String|Number} height Block height */ - setLndCfilterHeight(height) { + setLndCfilterHeight(height: number) { const heightAsNumber = Number(height) this.lndCfilterHeight = heightAsNumber this.emit(GOT_LND_CFILTER_HEIGHT, heightAsNumber) diff --git a/app/lib/zap/controller.js b/app/lib/zap/controller.js index 7333d7b2..6b4460ce 100644 --- a/app/lib/zap/controller.js +++ b/app/lib/zap/controller.js @@ -5,7 +5,6 @@ import pick from 'lodash.pick' import Store from 'electron-store' import StateMachine from 'javascript-state-machine' import LndConfig from '../lnd/config' -import Neutrino from '../lnd/neutrino' import Lightning from '../lnd/lightning' import { initWalletUnlocker } from '../lnd/walletUnlocker' import { mainLog } from '../utils/log' @@ -51,8 +50,8 @@ const grpcSslCipherSuites = connectionType => */ class ZapController { mainWindow: BrowserWindow - neutrino: Neutrino - lightning: Lightning + neutrino: any + lightning: any splashScreenTime: number lightningGrpcConnected: boolean lndConfig: LndConfig @@ -74,10 +73,10 @@ class ZapController { this.mainWindow = mainWindow // Keep a reference any neutrino process started by us. - this.neutrino = null + this.neutrino = undefined // Keep a reference to the lightning gRPC instance. - this.lightning = null + this.lightning = undefined // Time for the splash screen to remain visible. this.splashScreenTime = 500 @@ -303,7 +302,6 @@ class ZapController { */ startNeutrino() { mainLog.info('Starting Neutrino...') - this.neutrino = new Neutrino(this.lndConfig) this.neutrino.on('error', error => { mainLog.error(`Got error from lnd process: ${error})`) diff --git a/test/unit/lnd/neutrino.spec.js b/test/unit/lnd/neutrino.spec.js new file mode 100644 index 00000000..c89c3489 --- /dev/null +++ b/test/unit/lnd/neutrino.spec.js @@ -0,0 +1,171 @@ +import Neutrino from 'lib/lnd/neutrino' + +jest.mock('electron', () => { + const { normalize } = require('path') + + return { + app: { + getPath: name => normalize(`/tmp/zap-test/${name}`), + getAppPath: () => normalize('/tmp/zap-test') + } + } +}) + +describe('Neutrino', function() { + describe('Constructor', () => { + beforeAll(() => (this.neutrino = new Neutrino())) + + describe('initial values', () => { + it('should set the "process" property to null', () => { + expect(this.neutrino.process).toEqual(null) + }) + it('should set the "walletUnlockerGrpcActive" property to false', () => { + expect(this.neutrino.walletUnlockerGrpcActive).toEqual(false) + }) + it('should set the "lightningGrpcActive" property to false', () => { + expect(this.neutrino.lightningGrpcActive).toEqual(false) + }) + it('should set the "chainSyncStatus" property to "chain-sync-pending"', () => { + expect(this.neutrino.chainSyncStatus).toEqual('chain-sync-pending') + }) + it('should set the "currentBlockHeight" property to 0', () => { + expect(this.neutrino.currentBlockHeight).toEqual(0) + }) + it('should set the "lndBlockHeight" property to 0', () => { + expect(this.neutrino.lndBlockHeight).toEqual(0) + }) + it('should set the "lndCfilterHeight" property to 0', () => { + expect(this.neutrino.lndCfilterHeight).toEqual(0) + }) + }) + }) + + describe('.setState', () => { + describe('called with new state', () => { + beforeEach(() => { + this.neutrino = new Neutrino() + this.callback = jest.fn() + this.newVal = 'chain-sync-finished' + this.neutrino.on('chain-sync-finished', this.callback) + this.neutrino.setState(this.newVal) + }) + + it('should set the state', () => { + expect(this.neutrino.chainSyncStatus).toEqual(this.newVal) + }) + it('should emit an event with the new state name', () => { + expect(this.callback).toHaveBeenCalledTimes(1) + }) + }) + describe('called with current state', () => { + beforeEach(() => { + this.neutrino = new Neutrino() + this.callback = jest.fn() + this.newVal = 'chain-sync-pending' + this.neutrino.on('chain-sync-pending', this.callback) + this.neutrino.setState(this.newVal) + }) + + it('should not change the state', () => { + expect(this.neutrino.chainSyncStatus).toEqual(this.newVal) + }) + it('should not emit an event with the new state name', () => { + expect(this.callback).not.toHaveBeenCalled() + }) + }) + }) + + describe('.setCurrentBlockHeight', () => { + describe('called with higher height', () => { + beforeEach(() => { + this.neutrino = new Neutrino() + this.callback = jest.fn() + this.newVal = 100 + this.neutrino.on('got-current-block-height', this.callback) + this.neutrino.setCurrentBlockHeight(this.newVal) + }) + + it('should change the current block height', () => { + expect(this.neutrino.currentBlockHeight).toEqual(this.newVal) + }) + it('should emit an event with the new current block height', () => { + expect(this.callback).toHaveBeenCalledTimes(1) + expect(this.callback).toHaveBeenCalledWith(this.newVal) + }) + }) + describe('called with lower height', () => { + beforeEach(() => { + this.neutrino = new Neutrino() + this.callback = jest.fn() + this.newVal = -1 + this.neutrino.on('got-current-block-height', this.callback) + this.neutrino.setCurrentBlockHeight(this.newVal) + }) + + it('should not change the current block height', () => { + expect(this.neutrino.currentBlockHeight).toEqual(0) + }) + it('should not emit an event with the new current block height', () => { + expect(this.callback).not.toHaveBeenCalled() + }) + }) + }) + + describe('.setLndBlockHeight', () => { + describe('called with higher height', () => { + beforeEach(() => { + this.neutrino = new Neutrino() + this.callback = jest.fn() + this.newVal = 100 + this.neutrino.on('got-lnd-block-height', this.callback) + this.neutrino.setCurrentBlockHeight = jest.fn() + this.neutrino.setLndBlockHeight(this.newVal) + }) + + it('should change the lnd block height', () => { + expect(this.neutrino.lndBlockHeight).toEqual(this.newVal) + }) + it('should emit an event with the new lnd block height', () => { + expect(this.callback).toHaveBeenCalledTimes(1) + expect(this.callback).toHaveBeenCalledWith(this.newVal) + }) + it('should call this.setCurrentBlockHeight', () => { + expect(this.neutrino.setCurrentBlockHeight).toHaveBeenCalledTimes(1) + expect(this.neutrino.setCurrentBlockHeight).toHaveBeenCalledWith(this.newVal) + }) + }) + describe('called with lower height', () => { + beforeEach(() => { + this.neutrino = new Neutrino() + this.callback = jest.fn() + this.newVal = -1 + this.neutrino.on('got-lnd-block-height', this.callback) + this.neutrino.setLndBlockHeight(this.newVal) + this.neutrino.setCurrentBlockHeight = jest.fn() + }) + + it('should not change the lnd block height', () => { + expect(this.neutrino.lndBlockHeight).toEqual(0) + }) + it('should not emit an event with the new lnd block height', () => { + expect(this.callback).not.toHaveBeenCalled() + }) + it('should not call this.setCurrentBlockHeight', () => { + expect(this.neutrino.setCurrentBlockHeight).not.toHaveBeenCalled() + }) + }) + }) + + describe('.is', () => { + describe('called with current state', () => { + beforeEach(() => (this.neutrino = new Neutrino())) + + it('should returnn true if the current state matches', () => { + expect(this.neutrino.is('chain-sync-pending')).toEqual(true) + }) + it('should return false if the current state does not matche', () => { + expect(this.neutrino.is('some-other-state')).toEqual(false) + }) + }) + }) +})