From becb5ecee94122f21f95773d1166c99bc4ea6b7f Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Tue, 26 Jun 2018 10:01:35 +0200 Subject: [PATCH] refactor: modularize and simplify app startup code `main.dev.js` was hard to follow and understand the code flow. It housed code to handle quite a few distinct things. This is a fairly substantial refactor of the application startup code in which we: A new `Neutrino` class has been created with 2 public methods. `start` and `stop`. `start` will launch a new `lnd` process whilst `stop` will stop it. This class extends the `EventEmitter` class and emits the following events based on activity detected from the lnd log output. - `grpc-proxy-started` - gRPC started - `wallet-opened` Wallet opened - `fully-synced` - lnd is all caught up to the blockchain - `got-block-height` - got updated block height A new `ZapController` class has been created which houses all of the logic for intraprocess communication between the main and renderer processes. Previously we had several `setInterval` loops that were checking every second to see if the application status has changed and trigging the appropriate action if so. This was pretty hard to follow has been replaced here with more extensive use of promises. This enables us to act instantly to relevant changes rather than waiting up to 1 second for the next interval to fire. Now, the only stuff that lives in `main.dev.js` now is the top level `app` listeners, which calls out the other parts mentioned above to bootstrap the application. --- app/lnd/index.js | 6 +- app/lnd/lib/neutrino.js | 85 ++++++++ app/lnd/lib/util.js | 28 +++ app/main.dev.js | 437 ++++++++-------------------------------- app/reducers/lnd.js | 2 +- app/zap.js | 217 ++++++++++++++++++++ 6 files changed, 414 insertions(+), 361 deletions(-) create mode 100644 app/lnd/lib/neutrino.js create mode 100644 app/lnd/lib/util.js create mode 100644 app/zap.js diff --git a/app/lnd/index.js b/app/lnd/index.js index 9f423250..b5d27014 100644 --- a/app/lnd/index.js +++ b/app/lnd/index.js @@ -1,6 +1,7 @@ import config from './config' import lightning from './lib/lightning' import walletUnlocker from './lib/walletUnlocker' +import isLndRunning from './lib/util' import subscribe from './subscribe' import methods from './methods' import walletUnlockerMethods from './walletUnlockerMethods' @@ -10,7 +11,6 @@ import { mainLog } from '../utils/log' const initLnd = () => { const lndConfig = config.lnd() const lnd = lightning(lndConfig.lightningRpc, lndConfig.lightningHost) - const lndSubscribe = mainWindow => subscribe(mainWindow, lnd, mainLog) const lndMethods = (event, msg, data) => methods(lnd, mainLog, event, msg, data) @@ -22,7 +22,6 @@ const initLnd = () => { const initWalletUnlocker = () => { const lndConfig = config.lnd() - const walletUnlockerObj = walletUnlocker(lndConfig.lightningRpc, lndConfig.lightningHost) const walletUnlockerMethodsCallback = (event, msg, data) => walletUnlockerMethods(walletUnlockerObj, mainLog, event, msg, data) @@ -32,5 +31,6 @@ const initWalletUnlocker = () => { export default { initLnd, - initWalletUnlocker + initWalletUnlocker, + isLndRunning } diff --git a/app/lnd/lib/neutrino.js b/app/lnd/lib/neutrino.js new file mode 100644 index 00000000..992a256f --- /dev/null +++ b/app/lnd/lib/neutrino.js @@ -0,0 +1,85 @@ +import split2 from 'split2' +import { spawn } from 'child_process' +import EventEmitter from 'events' +import config from '../config' +import { mainLog, lndLog, lndLogGetLevel } from '../../utils/log' + +class Neutrino extends EventEmitter { + constructor(alias, autopilot) { + super() + this.alias = alias + this.autopilot = autopilot + this.process = null + } + + start() { + if (this.process) { + throw new Error('Neutrino process with PID ${this.process.pid} already exists.') + } + + const lndConfig = config.lnd() + mainLog.info('Starting lnd in neutrino mode') + mainLog.debug(' > lndPath', lndConfig.lndPath) + mainLog.debug(' > lightningRpc:', lndConfig.lightningRpc) + mainLog.debug(' > lightningHost:', lndConfig.lightningHost) + mainLog.debug(' > cert:', lndConfig.cert) + mainLog.debug(' > macaroon:', lndConfig.macaroon) + + const neutrinoArgs = [ + `--configfile=${lndConfig.configPath}`, + `${this.autopilot ? '--autopilot.active' : ''}`, + `${this.alias ? `--alias=${this.alias}` : ''}` + ] + + this.process = spawn(lndConfig.lndPath, neutrinoArgs) + .on('error', error => this.emit('error', error)) + .on('close', code => { + this.emit('close', code) + this.process = null + }) + + // Listen for when neutrino prints odata to stderr. + this.process.stderr.pipe(split2()).on('data', line => { + if (process.env.NODE_ENV === 'development') { + lndLog[lndLogGetLevel(line)](line) + } + }) + + // Listen for when neutrino prints data to stdout. + this.process.stdout.pipe(split2()).on('data', line => { + if (process.env.NODE_ENV === 'development') { + lndLog[lndLogGetLevel(line)](line) + } + + // gRPC started. + if (line.includes('gRPC proxy started') && line.includes('password')) { + this.emit('grpc-proxy-started') + } + + // Wallet opened. + if (line.includes('gRPC proxy started') && !line.includes('password')) { + this.emit('wallet-opened') + } + + // LND is all caught up to the blockchain. + if (line.includes('Chain backend is fully synced')) { + this.emit('fully-synced') + } + + // Pass current block height progress to front end for loading state UX + if (line.includes('Caught up to height') || line.includes('Catching up block hashes')) { + this.emit('got-block-height', line) + } + }) + return this.process + } + + stop() { + if (this.process) { + this.process.kill() + this.process = null + } + } +} + +export default Neutrino diff --git a/app/lnd/lib/util.js b/app/lnd/lib/util.js new file mode 100644 index 00000000..0afe5e02 --- /dev/null +++ b/app/lnd/lib/util.js @@ -0,0 +1,28 @@ +import { lookup } from 'ps-node' +import { mainLog } from '../../utils/log' + +/** + * Check to see if an LND process is running. + * @return {Promise} Boolean indicating wether an existing lnd process was found on the host machine. + */ +const isLndRunning = () => { + return new Promise((resolve, reject) => { + mainLog.info('Looking for existing lnd process') + lookup({ command: 'lnd' }, (err, results) => { + // There was an error checking for the LND process. + if (err) { + return reject(err) + } + + if (!results.length) { + // An LND process was found, no need to start our own. + mainLog.info('Existing lnd process not found') + return resolve(false) + } + mainLog.info('Found existing lnd process') + return resolve(true) + }) + }) +} + +export default isLndRunning diff --git a/app/main.dev.js b/app/main.dev.js index a52c3d4b..7cd5e7b4 100644 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -1,5 +1,3 @@ -/* eslint global-require: 1, flowtype-errors/show-errors: 0 */ - /** * This module executes inside of electron's main process. You can start * electron renderer process from here and communicate with the other processes @@ -7,369 +5,105 @@ * * When running `npm run build` or `npm run build-main`, this file is compiled to * `./app/main.prod.js` using webpack. This gives us some performance wins. - * - * */ -import { app, BrowserWindow, ipcMain, dialog, session } from 'electron' -import path from 'path' -import fs from 'fs' -import split2 from 'split2' -import { spawn } from 'child_process' -import { lookup } from 'ps-node' -import Store from 'electron-store' +import { app, BrowserWindow, session } from 'electron' +import { mainLog } from './utils/log' import MenuBuilder from './menu' +import ZapController from './zap' import lnd from './lnd' -import config from './lnd/config' -import { mainLog, lndLog, lndLogGetLevel } from './utils/log' - -let mainWindow = null - -let didFinishLoad = false - -let startedSync = false -let sentGrpcDisconnect = false - -let neutrino = null - -if (process.env.NODE_ENV === 'production') { - const sourceMapSupport = require('source-map-support') - sourceMapSupport.install() -} - -if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { - require('electron-debug')() - const p = path.join(__dirname, '..', 'app', 'node_modules') - require('module').globalPaths.push(p) -} - -const installExtensions = async () => { - const installer = require('electron-devtools-installer') - const forceDownload = !!process.env.UPGRADE_EXTENSIONS - const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'] - - return Promise.all( - extensions.map(name => installer.default(installer[name], forceDownload)) - ).catch(mainLog.error) -} - -// Send the front end event letting them know the gRPC connection is disconnected -const sendGrpcDisconnected = () => { - const sendGrpcDisonnectedInterval = setInterval(() => { - if (didFinishLoad) { - clearInterval(sendGrpcDisonnectedInterval) - - if (mainWindow) { - sentGrpcDisconnect = true - mainWindow.webContents.send('grpcDisconnected') - } - } - }, 1000) -} - -// Send the front end event letting them know LND is synced to the blockchain -const sendLndSyncing = () => { - const sendLndSyncingInterval = setInterval(() => { - if (didFinishLoad) { - clearInterval(sendLndSyncingInterval) - - if (mainWindow) { - mainLog.info('SENDING SYNCING') - startedSync = true - mainWindow.webContents.send('lndSyncing') - } - } - }, 1000) -} - -const sendStartOnboarding = () => { - const sendStartOnboardingInterval = setInterval(() => { - if (didFinishLoad) { - clearInterval(sendStartOnboardingInterval) - - if (mainWindow) { - mainLog.timeEnd('Time until onboarding has started') - mainLog.info('STARTING ONBOARDING') - mainWindow.webContents.send('startOnboarding') - } - } - }, 1000) -} - -// Send the front end event letting them know the gRPC connection has started -const sendGrpcConnected = () => { - const sendGrpcConnectedInterval = setInterval(() => { - if (didFinishLoad && sentGrpcDisconnect) { - clearInterval(sendGrpcConnectedInterval) - - if (mainWindow) { - mainWindow.webContents.send('grpcConnected') - } - } - }, 1000) -} - -// Create and subscribe the grpc object -const startGrpc = () => { - mainLog.info('Starting gRPC...') - try { - const { lndSubscribe, lndMethods } = lnd.initLnd() - - // Subscribe to bi-directional streams - lndSubscribe(mainWindow) - - // Listen for all gRPC restful methods - ipcMain.on('lnd', (event, { msg, data }) => { - lndMethods(event, msg, data) - }) - sendGrpcConnected() - } catch (error) { - dialog.showMessageBox({ - type: 'error', - message: `Unable to connect to lnd. Please check your lnd node and try again: ${error}` - }) - app.quit() - } -} - -// Create and subscribe the grpc object -const startWalletUnlocker = () => { - mainLog.info('Starting wallet unlocker...') - try { - const walletUnlockerMethods = lnd.initWalletUnlocker() - - // Listen for all gRPC restful methods - ipcMain.on('walletUnlocker', (event, { msg, data }) => { - walletUnlockerMethods(event, msg, data) - }) - - mainWindow.webContents.send('walletUnlockerStarted') - } catch (error) { - dialog.showMessageBox({ - type: 'error', - message: `Unable to start lnd wallet unlocker. Please check your lnd node and try again: ${error}` - }) - app.quit() - } -} - -// Send the front end event letting them know LND is synced to the blockchain -const sendLndSynced = () => { - const sendLndSyncedInterval = setInterval(() => { - if (didFinishLoad && startedSync) { - clearInterval(sendLndSyncedInterval) - - if (mainWindow) { - mainLog.info('SENDING SYNCED') - mainWindow.webContents.send('lndSynced') - } - } - }, 1000) -} - -// Starts the LND node -const startLnd = (alias, autopilot) => { - const lndConfig = config.lnd() - mainLog.info('STARTING BUNDLED LND') - mainLog.debug(' > lndPath', lndConfig.lndPath) - mainLog.debug(' > lightningRpc:', lndConfig.lightningRpc) - mainLog.debug(' > lightningHost:', lndConfig.lightningHost) - mainLog.debug(' > cert:', lndConfig.cert) - mainLog.debug(' > macaroon:', lndConfig.macaroon) - - const neutrinoArgs = [ - `--configfile=${lndConfig.configPath}`, - `${autopilot ? '--autopilot.active' : ''}`, - `${alias ? `--alias=${alias}` : ''}` - ] - - neutrino = spawn(lndConfig.lndPath, neutrinoArgs) - .on('error', error => { - lndLog.error(`lnd error: ${error}`) - dialog.showMessageBox({ - type: 'error', - message: `lnd error: ${error}` - }) - }) - .on('close', code => { - lndLog.info(`lnd shutting down ${code}`) - app.quit() - }) - - // Listen for when neutrino prints odata to stderr. - neutrino.stderr.pipe(split2()).on('data', line => { - if (process.env.NODE_ENV === 'development') { - lndLog[lndLogGetLevel(line)](line) - } +// Set up a couple of timers to track the app startup progress. +mainLog.time('Time until app is ready') +mainLog.time('Time until lnd process lookup finished') + +// Determine wether we should start our own lnd process or connect to one that is already running. +const zapMode = lnd + .isLndRunning() + .then(res => { + mainLog.debug('lnd already running: %s', res) + mainLog.timeEnd('Time until lnd process lookup finished') + return res ? 'external' : 'internal' }) - - // Listen for when neutrino prints data to stdout. - neutrino.stdout.pipe(split2()).on('data', line => { - if (process.env.NODE_ENV === 'development') { - lndLog[lndLogGetLevel(line)](line) - } - - // If the gRPC proxy has started we can start ours - if (line.includes('gRPC proxy started')) { - const certInterval = setInterval(() => { - if (fs.existsSync(lndConfig.cert)) { - clearInterval(certInterval) - - mainLog.info('CERT EXISTS, STARTING WALLET UNLOCKER') - startWalletUnlocker() - - if (mainWindow) { - mainWindow.webContents.send('walletUnlockerStarted') - } - } - }, 1000) - } - - if (line.includes('gRPC proxy started') && !line.includes('password')) { - mainLog.info('WALLET OPENED, STARTING LIGHTNING GRPC CONNECTION') - sendLndSyncing() - startGrpc() - } - - // Pass current clock height progress to front end for loading state UX - if ( - mainWindow && - (line.includes('Caught up to height') || line.includes('Catching up block hashes to height')) - ) { - // const blockHeight = line.slice(line.indexOf('Caught up to height') + 'Caught up to height'.length).trim() - mainWindow.webContents.send('lndStdout', line) - } - - // When LND is all caught up to the blockchain - if (line.includes('Chain backend is fully synced')) { - // Log that LND is caught up to the current block height - mainLog.info('NEUTRINO IS SYNCED') - - // Let the front end know we have stopped syncing LND - sendLndSynced() - } - }) -} + .catch(mainLog.error) /** - * Add event listeners... + * Initialize Zap as soon as electron is ready. */ +app.on('ready', () => { + mainLog.timeEnd('Time until app is ready') -app.on('window-all-closed', () => { - // Respect the OSX convention of having the application in memory even - // after all windows have been closed - if (process.platform !== 'darwin') { - app.quit() - } -}) - -app.on('ready', async () => { + // Start a couple more timers to track the app loading time. mainLog.time('Time until app is visible') mainLog.time('Time until onboarding has started') - if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { - await installExtensions() - } - - mainWindow = new BrowserWindow({ + // Create the electron browser window. + const mainWindow = new BrowserWindow({ show: false, titleBarStyle: 'hidden', width: 950, height: 600, minWidth: 950, - minHeight: 425 + minHeight: 425, + backgroundColor: '#1c1e26' }) - if (process.env.HOT) { - const port = process.env.PORT || 1212 - mainWindow.loadURL(`http://localhost:${port}/dist/index.html`) - } else { - mainWindow.loadURL(`file://${__dirname}/dist/index.html`) + // Initialise the application. + const zap = new ZapController(mainWindow, zapMode) + zap.init() + + // Initialise the application menus. + const menuBuilder = new MenuBuilder(mainWindow) + menuBuilder.buildMenu() + + /** + * In production mode, enable source map support. + */ + if (process.env.NODE_ENV === 'production') { + const sourceMapSupport = require('source-map-support') // eslint-disable-line global-require + sourceMapSupport.install() } + /** + * In development mode or when DEBUG_PROD is set, enable debugging tools. + */ if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { + // eslint-disable global-require + require('electron-debug')() + const installer = require('electron-devtools-installer') + const forceDownload = !!process.env.UPGRADE_EXTENSIONS + const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'] + + Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))).catch( + mainLog.error + ) + mainWindow.webContents.once('dom-ready', () => { mainWindow.openDevTools() }) } - // @TODO: Use 'ready-to-show' event - // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event - mainWindow.webContents.on('did-finish-load', () => { - if (!mainWindow) { - throw new Error('"mainWindow" is not defined') + /** + * Add application event listener: + * - Kill lnd process is killed when the app quits. + */ + app.on('quit', () => { + mainLog.debug('app.quit') + if (zap.neutrino) { + zap.neutrino.stop() } - mainLog.timeEnd('Time until app is visible') - mainWindow.show() - mainWindow.focus() - - // now sync and grpc events can be fired to the front end - didFinishLoad = true }) - mainWindow.on('closed', () => { - mainWindow = null - - // shut down zap when a user closes the window - app.quit() - }) - - const menuBuilder = new MenuBuilder(mainWindow) - menuBuilder.buildMenu() - - sendGrpcDisconnected() - - mainLog.info('LOOKING FOR EXISTING LND PROCESS') - // Check to see if an LND process is running. - lookup({ command: 'lnd' }, (err, results) => { - // There was an error checking for the LND process. - if (err) { - throw new Error(err) - } - - if (!results.length) { - // An LND process was found, no need to start our own. - mainLog.info('EXISTING LND PROCESS NOT FOUND') - // Let the application know onboarding has started. - sendStartOnboarding() - } else { - // An LND process was found, no need to start our own. - mainLog.info('FOUND EXISTING LND PROCESS') - startGrpc() - mainWindow.webContents.send('successfullyCreatedWallet') - } - }) - - // Start LND - // once the onboarding has enough information, start or connect to LND. - ipcMain.on('startLnd', (event, options = {}) => { - const store = new Store({ name: 'connection' }) - store.store = { - type: options.connectionType, - host: options.connectionHost, - cert: options.connectionCert, - macaroon: options.connectionMacaroon, - alias: options.alias, - autopilot: options.autopilot - } - - mainLog.info('GOT LND CONFIG') - mainLog.debug(' > connectionType:', options.connectionType) - mainLog.debug(' > connectionHost:', options.connectionHost) - mainLog.debug(' > connectionCert:', options.connectionCert) - mainLog.debug(' > connectionMacaroon:', options.connectionMacaroon) - mainLog.debug(' > alias:', options.alias) - mainLog.debug(' > autopilot:', options.autopilot) - - mainLog.info('SAVED LND CONFIG TO:', store.path) - - if (options.connectionType === 'local') { - startLnd(options.alias, options.autopilot) - } else { - mainLog.info('CONNECTING TO CUSTOM LND INSTANCE') - startGrpc() - mainWindow.webContents.send('successfullyCreatedWallet') - } + /** + * Add application event listener: + * - Open zap payment form when lightning url is opened + */ + app.setAsDefaultProtocolClient('lightning') + app.on('open-url', (event, url) => { + mainLog.debug('open-url') + event.preventDefault() + const payreq = url.split(':')[1] + zap.sendMessage('lightningPaymentUri', { payreq }) + mainWindow.show() }) // HACK: patch webrequest to fix devtools incompatibility with electron 2.x. @@ -386,27 +120,16 @@ app.on('ready', async () => { callback({ cancel: false }) } }) -}) - -app.setAsDefaultProtocolClient('lightning') - -app.on('open-url', (event, url) => { - event.preventDefault() - if (!mainWindow) { - throw new Error('"mainWindow" is not defined') - } - - const payreq = url.split(':')[1] - mainWindow.webContents.send('lightningPaymentUri', { payreq }) - mainWindow.show() -}) - -// Ensure lnd process is killed when the app quits. -app.on('quit', () => { - if (neutrino) { - neutrino.kill() - } + /** + * Add application event listener: + * - quit app when window is closed + */ + app.on('window-all-closed', () => { + mainLog.debug('app.window-all-closed') + // Respect the OSX convention of having the application in memory even after all windows have been closed + if (process.platform !== 'darwin') { + app.quit() + } + }) }) - -export default { startLnd } diff --git a/app/reducers/lnd.js b/app/reducers/lnd.js index 7c45858c..e63a8bd6 100644 --- a/app/reducers/lnd.js +++ b/app/reducers/lnd.js @@ -113,7 +113,7 @@ const ACTION_HANDLERS = { // ------------------------------------ const initialState = { syncing: false, - grpcStarted: true, + grpcStarted: false, fetchingBlockHeight: false, lines: [], blockHeight: 0, diff --git a/app/zap.js b/app/zap.js new file mode 100644 index 00000000..f5600643 --- /dev/null +++ b/app/zap.js @@ -0,0 +1,217 @@ +import { app, ipcMain, dialog } from 'electron' +import Store from 'electron-store' +import lnd from './lnd' +import Neutrino from './lnd/lib/neutrino' +import { mainLog } from './utils/log' + +/** + * @class ZapController + * + * The ZapController class coordinates actions between the the main nand renderer processes. + */ +class ZapController { + /** + * Create a new ZapController instance. + * @param {BrowserWindow} mainWindow BrowserWindow instance to interact with + * @param {String|Promise} mode String or Promise that resolves to the desired run mode. Valid options are: + * - 'internal': start a new lnd process. + * - 'external': connect to an existing lnd process. + */ + constructor(mainWindow, mode) { + this.mode = mode + + // Variable to hold the main window instance. + this.mainWindow = mainWindow + + // Keep a reference any neutrino process started by us. + this.neutrino = null + + // Time for the splash screen to remain visible. + this.splashScreenTime = 500 + } + + /** + * Initialize the application. + */ + init() { + if (process.env.HOT) { + const port = process.env.PORT || 1212 + this.mainWindow.loadURL(`http://localhost:${port}/dist/index.html`) + } else { + this.mainWindow.loadURL(`file://${__dirname}/dist/index.html`) + } + + // Register IPC listeners so that we can react to instructions coming from the app. + this._registerIpcListeners() + + // Show the window as soon as the application has finished loading. + this.mainWindow.webContents.on('did-finish-load', async () => { + this.mainWindow.show() + this.mainWindow.focus() + mainLog.timeEnd('Time until app is visible') + mainLog.time('Time until we know the run mode') + + Promise.resolve(this.mode) + .then(mode => { + const timeUntilWeKnowTheRunMode = mainLog.timeEnd('Time until we know the run mode') + return setTimeout(() => { + if (mode === 'external') { + // If lnd is already running, create and subscribe to the Lightning grpc object. + this.startGrpc() + this.sendMessage('successfullyCreatedWallet') + } else { + // Otherwise, start the onboarding process. + this.sendMessage('startOnboarding') + mainLog.timeEnd('Time until onboarding has started') + } + }, timeUntilWeKnowTheRunMode < this.splashScreenTime ? this.splashScreenTime : 0) + }) + .catch(mainLog.error) + }) + + this.mainWindow.on('closed', () => { + this.mainWindow = null + + // shut down zap when a user closes the window + app.quit() + }) + } + + /** + * Send a message to the main window. + * @param {string} msg message to send. + * @param {[type]} data additional data to acompany the message. + */ + sendMessage(msg, data) { + mainLog.info('Sending message to renderer process: %o', { msg, data }) + this.mainWindow.webContents.send(msg, data) + } + + /** + * Create and subscribe to the Lightning grpc object. + */ + startGrpc() { + mainLog.info('Starting gRPC...') + try { + const { lndSubscribe, lndMethods } = lnd.initLnd() + + // Subscribe to bi-directional streams + lndSubscribe(this.mainWindow) + + // Listen for all gRPC restful methods + ipcMain.on('lnd', (event, { msg, data }) => { + lndMethods(event, msg, data) + }) + + this.sendMessage('grpcConnected') + } catch (error) { + dialog.showMessageBox({ + type: 'error', + message: `Unable to connect to lnd. Please check your lnd node and try again: ${error}` + }) + app.quit() + } + } + + /** + * Create and subscribe to the WalletUnlocker grpc object. + */ + startWalletUnlocker() { + mainLog.info('Starting wallet unlocker...') + try { + const walletUnlockerMethods = lnd.initWalletUnlocker() + + // Listen for all gRPC restful methods + ipcMain.on('walletUnlocker', (event, { msg, data }) => { + walletUnlockerMethods(event, msg, data) + }) + + this.sendMessage('walletUnlockerStarted') + } catch (error) { + dialog.showMessageBox({ + type: 'error', + message: `Unable to start lnd wallet unlocker. Please check your lnd node and try again: ${error}` + }) + app.quit() + } + } + + /** + * Starts the LND node and attach event listeners. + * @param {string} alias Alias to assign to the lnd node. + * @param {boolean} autopilot True if autopilot should be enabled. + * @return {Neutrino} Neutrino instance. + */ + startLnd(alias, autopilot) { + this.neutrino = new Neutrino(alias, autopilot) + + this.neutrino.on('error', error => { + mainLog.error(`Got error from lnd process: ${error})`) + dialog.showMessageBox({ + type: 'error', + message: `lnd error: ${error}` + }) + }) + + this.neutrino.on('close', code => { + mainLog.info(`Lnd process has shut down (code ${code})`) + app.quit() + }) + + this.neutrino.on('grpc-proxy-started', () => { + mainLog.info('gRPC proxy started') + this.startWalletUnlocker() + }) + + this.neutrino.on('wallet-opened', () => { + mainLog.info('Wallet opened') + this.startGrpc() + this.sendMessage('lndSyncing') + }) + + this.neutrino.on('fully-synced', () => { + mainLog.info('Neutrino fully synced') + this.sendMessage('lndSynced') + }) + + this.neutrino.on('got-block-height', line => { + this.sendMessage('lndStdout', line) + }) + + this.neutrino.start() + } + + /** + * Add IPC event listeners... + */ + _registerIpcListeners() { + ipcMain.on('startLnd', (event, options = {}) => { + const store = new Store({ name: 'connection' }) + store.store = { + type: options.connectionType, + host: options.connectionHost, + cert: options.connectionCert, + macaroon: options.connectionMacaroon, + alias: options.alias, + autopilot: options.autopilot + } + mainLog.info('Saved lnd config to:', store.path) + + if (options.connectionType === 'local') { + mainLog.info('Starting new lnd instance') + mainLog.debug(' > alias:', options.alias) + mainLog.debug(' > autopilot:', options.autopilot) + this.startLnd(options.alias, options.autopilot) + } else { + mainLog.info('Connecting to custom lnd instance') + mainLog.debug(' > connectionHost:', options.connectionHost) + mainLog.debug(' > connectionCert:', options.connectionCert) + mainLog.debug(' > connectionMacaroon:', options.connectionMacaroon) + this.startGrpc() + this.sendMessage('successfullyCreatedWallet') + } + }) + } +} + +export default ZapController