diff --git a/app/lnd/index.js b/app/lnd/index.js index 4a87eda3..404e5a3b 100644 --- a/app/lnd/index.js +++ b/app/lnd/index.js @@ -1,24 +1,9 @@ import config from './config' -import lightning from './lib/lightning' import walletUnlocker from './lib/walletUnlocker' -import subscribe from './subscribe' -import methods from './methods' import walletUnlockerMethods from './walletUnlockerMethods' // use mainLog because lndLog is reserved for the lnd binary itself import { mainLog } from '../utils/log' -const initLnd = async () => { - const lnd = await lightning() - - const lndSubscribe = mainWindow => subscribe(mainWindow, lnd, mainLog) - const lndMethods = (event, msg, data) => methods(lnd, mainLog, event, msg, data) - - return Promise.resolve({ - lndSubscribe, - lndMethods - }) -} - const initWalletUnlocker = () => { const lndConfig = config.lnd() const walletUnlockerObj = walletUnlocker(lndConfig.rpcProtoPath, lndConfig.host) @@ -29,6 +14,5 @@ const initWalletUnlocker = () => { } export default { - initLnd, initWalletUnlocker } diff --git a/app/lnd/lib/lightning.js b/app/lnd/lib/lightning.js index fb04c0cb..fc28979e 100644 --- a/app/lnd/lib/lightning.js +++ b/app/lnd/lib/lightning.js @@ -2,52 +2,112 @@ import grpc from 'grpc' import { loadSync } from '@grpc/proto-loader' import config from '../config' import { getDeadline, validateHost, createSslCreds, createMacaroonCreds } from './util' +import methods from '../methods' +import { mainLog } from '../../utils/log' +import subscribeToTransactions from '../subscribe/transactions' +import subscribeToInvoices from '../subscribe/invoices' +import subscribeToChannelGraph from '../subscribe/channelgraph' /** * Creates an LND grpc client lightning service. - * @returns {rpc.lnrpc.Lightning} + * @returns {Lightning} */ -const lightning = async () => { - const lndConfig = config.lnd() - const { host, rpcProtoPath, cert, macaroon } = lndConfig - - // Verify that the host is valid before creating a gRPC client that is connected to it. - return await validateHost(host).then(async () => { - // Load the gRPC proto file. - // The following options object closely approximates the existing behavior of grpc.load. - // See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md - const options = { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true +class Lightning { + constructor() { + this.mainWindow = null + this.lnd = null + this.subscriptions = { + channelGraph: null, + invoices: null, + transactions: null } - const packageDefinition = loadSync(rpcProtoPath, options) - - // Load gRPC package definition as a gRPC object hierarchy. - const rpc = grpc.loadPackageDefinition(packageDefinition) - - // Create ssl and macaroon credentials to use with the gRPC client. - const [sslCreds, macaroonCreds] = await Promise.all([ - createSslCreds(cert), - createMacaroonCreds(macaroon) - ]) - const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds) - - // Instantiate a new connection to the Lightning interface. - const lnd = new rpc.lnrpc.Lightning(host, credentials) - - // Call the getInfo method to ensure that we can make successful calls to the gRPC interface. - return new Promise((resolve, reject) => { - lnd.getInfo({}, { deadline: getDeadline(2) }, err => { - if (err) { - return reject(err) - } - return resolve(lnd) + } + + /** + * Connect to the gRPC interface and verify it is functional. + * @return {Promise} + */ + async connect() { + const lndConfig = config.lnd() + const { host, rpcProtoPath, cert, macaroon } = lndConfig + + // Verify that the host is valid before creating a gRPC client that is connected to it. + return await validateHost(host).then(async () => { + // Load the gRPC proto file. + // The following options object closely approximates the existing behavior of grpc.load. + // See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md + const options = { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + } + const packageDefinition = loadSync(rpcProtoPath, options) + + // Load gRPC package definition as a gRPC object hierarchy. + const rpc = grpc.loadPackageDefinition(packageDefinition) + + // Create ssl and macaroon credentials to use with the gRPC client. + const [sslCreds, macaroonCreds] = await Promise.all([ + createSslCreds(cert), + createMacaroonCreds(macaroon) + ]) + const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds) + + // Create a new gRPC client instance. + const lnd = new rpc.lnrpc.Lightning(host, credentials) + + // Call the getInfo method to ensure that we can make successful calls to the gRPC interface. + return new Promise((resolve, reject) => { + lnd.getInfo({}, { deadline: getDeadline(2) }, err => { + if (err) { + return reject(err) + } + this.lnd = lnd + return resolve(lnd) + }) }) }) - }) + } + + /** + * Discomnnect the gRPC service. + */ + disconnect() { + this.unsubscribe() + this.lnd.close() + } + + /** + * Hook up lnd restful methods. + */ + lndMethods(event, msg, data) { + return methods(this.lnd, mainLog, event, msg, data) + } + + /** + * Subscribe to all bi-directional streams. + */ + subscribe(mainWindow) { + this.mainWindow = mainWindow + this.subscriptions.channelGraph = subscribeToChannelGraph(this.mainWindow, this.lnd, mainLog) + this.subscriptions.invoices = subscribeToInvoices(this.mainWindow, this.lnd, mainLog) + this.subscriptions.transactions = subscribeToTransactions(this.mainWindow, this.lnd, mainLog) + } + + /** + * Unsubscribe from all bi-directional streams. + */ + unsubscribe() { + Object.keys(this.subscriptions).forEach(subscription => { + if (this.subscriptions[subscription]) { + this.subscriptions[subscription].cancel() + this.subscriptions[subscription] = null + } + }) + this.mainWindow = null + } } -export default lightning +export default Lightning diff --git a/app/lnd/lib/util.js b/app/lnd/lib/util.js index 079b6239..381939cb 100644 --- a/app/lnd/lib/util.js +++ b/app/lnd/lib/util.js @@ -2,6 +2,7 @@ import dns from 'dns' import fs from 'fs' import axios from 'axios' import { promisify } from 'util' +import { lookup } from 'ps-node' import path from 'path' import grpc from 'grpc' import isIP from 'validator/lib/isIP' @@ -137,3 +138,27 @@ export const createMacaroonCreds = async macaroonPath => { callback(null, metadata) ) } + +/** + * Check to see if an LND process is running. + * @return {Promise} Boolean indicating wether an existing lnd process was found on the host machine. + */ +export 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) + }) + }) +} diff --git a/app/lnd/subscribe/channelgraph.js b/app/lnd/subscribe/channelgraph.js index 5f63948f..02f30a08 100644 --- a/app/lnd/subscribe/channelgraph.js +++ b/app/lnd/subscribe/channelgraph.js @@ -1,8 +1,14 @@ -export default function subscribeToChannelGraph(mainWindow, lnd) { +import { status } from 'grpc' + +export default function subscribeToChannelGraph(mainWindow, lnd, log) { const call = lnd.subscribeChannelGraph({}) call.on('data', channelGraphData => mainWindow.send('channelGraphData', { channelGraphData })) + call.on('end', () => log.info('end')) + call.on('error', error => error.code !== status.CANCELLED && log.error(error)) call.on('status', channelGraphStatus => mainWindow.send('channelGraphStatus', { channelGraphStatus }) ) + + return call } diff --git a/app/lnd/subscribe/index.js b/app/lnd/subscribe/index.js deleted file mode 100644 index cfab8f93..00000000 --- a/app/lnd/subscribe/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import subscribeToTransactions from './transactions' -import subscribeToInvoices from './invoices' -import subscribeToChannelGraph from './channelgraph' - -export default (mainWindow, lnd, log) => { - subscribeToTransactions(mainWindow, lnd, log) - subscribeToInvoices(mainWindow, lnd, log) - subscribeToChannelGraph(mainWindow, lnd) -} diff --git a/app/lnd/subscribe/invoices.js b/app/lnd/subscribe/invoices.js index 8766cb89..a127c221 100644 --- a/app/lnd/subscribe/invoices.js +++ b/app/lnd/subscribe/invoices.js @@ -1,8 +1,15 @@ +import { status } from 'grpc' + export default function subscribeToInvoices(mainWindow, lnd, log) { const call = lnd.subscribeInvoices({}) - call.on('data', invoice => mainWindow.send('invoiceUpdate', { invoice })) + call.on('data', invoice => { + log.info('INVOICE:', invoice) + mainWindow.send('invoiceUpdate', { invoice }) + }) call.on('end', () => log.info('end')) - call.on('error', error => log.error(error)) - call.on('status', status => log.info('status:', status)) + call.on('error', error => error.code !== status.CANCELLED && log.error(error)) + call.on('status', status => log.info('INVOICE STATUS:', status)) + + return call } diff --git a/app/lnd/subscribe/transactions.js b/app/lnd/subscribe/transactions.js index f1b462a5..0f143dc5 100644 --- a/app/lnd/subscribe/transactions.js +++ b/app/lnd/subscribe/transactions.js @@ -1,10 +1,15 @@ +import { status } from 'grpc' + export default function subscribeToTransactions(mainWindow, lnd, log) { const call = lnd.subscribeTransactions({}) + call.on('data', transaction => { log.info('TRANSACTION:', transaction) mainWindow.send('newTransaction', { transaction }) }) call.on('end', () => log.info('end')) - call.on('error', error => log.error('error: ', error)) + call.on('error', error => error.code !== status.CANCELLED && log.error(error)) call.on('status', status => log.info('TRANSACTION STATUS: ', status)) + + return call } diff --git a/app/lnd/walletUnlockerMethods/index.js b/app/lnd/walletUnlockerMethods/index.js index 9128a44d..de36f493 100644 --- a/app/lnd/walletUnlockerMethods/index.js +++ b/app/lnd/walletUnlockerMethods/index.js @@ -33,7 +33,7 @@ export default function(walletUnlocker, log, event, msg, data) { case 'initWallet': walletController .initWallet(walletUnlocker, data) - .then(() => event.sender.send('successfullyCreatedWallet')) + .then(() => event.sender.send('finishOnboarding')) .catch(error => log.error('initWallet:', error)) break default: diff --git a/app/main.dev.js b/app/main.dev.js index b104cef9..af82dbf7 100644 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -14,7 +14,6 @@ import ZapUpdater from './updater' // 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') /** * Initialize Zap as soon as electron is ready. @@ -22,11 +21,7 @@ mainLog.time('Time until lnd process lookup finished') app.on('ready', () => { mainLog.timeEnd('Time until app is ready') - // 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') - - // Create the electron browser window. + // Create a new browser window. const mainWindow = new BrowserWindow({ show: false, titleBarStyle: 'hidden', @@ -71,22 +66,11 @@ app.on('ready', () => { mainLog.error ) - mainWindow.webContents.once('dom-ready', () => { - mainWindow.openDevTools() + zap.mainWindow.webContents.once('dom-ready', () => { + zap.mainWindow.openDevTools() }) } - /** - * 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() - } - }) - /** * Add application event listener: * - Open zap payment form when lightning url is opened @@ -97,7 +81,7 @@ app.on('ready', () => { event.preventDefault() const payreq = url.split(':')[1] zap.sendMessage('lightningPaymentUri', { payreq }) - mainWindow.show() + zap.mainWindow.show() }) // HACK: patch webrequest to fix devtools incompatibility with electron 2.x. @@ -126,4 +110,24 @@ app.on('ready', () => { app.quit() } }) + + /** + * Add application event listener: + * - Stop gRPC and kill lnd process before the app windows are closed and the app quits. + */ + app.on('before-quit', async event => { + if (zap.state !== 'terminated') { + event.preventDefault() + zap.terminate() + } else { + zap.mainWindow.forceClose = true + } + }) + + /** + * On OS X it's common to re-open a window in the app when the dock icon is clicked. + */ + app.on('activate', () => { + zap.mainWindow.show() + }) }) diff --git a/app/reducers/channels.js b/app/reducers/channels.js index 381d20f2..bde39df5 100644 --- a/app/reducers/channels.js +++ b/app/reducers/channels.js @@ -1,7 +1,7 @@ import { createSelector } from 'reselect' import { ipcRenderer } from 'electron' -import { btc } from 'utils' -import { showNotification } from 'notifications' +import { showNotification } from '../notifications' +import { btc } from '../utils' import { requestSuggestedNodes } from '../api' import { setError } from './error' // ------------------------------------ diff --git a/app/reducers/ipc.js b/app/reducers/ipc.js index 6336b2e9..b4fa10ef 100644 --- a/app/reducers/ipc.js +++ b/app/reducers/ipc.js @@ -44,7 +44,7 @@ import { walletUnlockerGrpcActive, receiveSeed, receiveSeedError, - successfullyCreatedWallet, + finishOnboarding, walletUnlocked, unlockWalletError } from './onboarding' @@ -111,7 +111,7 @@ const ipc = createIpc({ walletUnlockerGrpcActive, receiveSeed, receiveSeedError, - successfullyCreatedWallet, + finishOnboarding, walletUnlocked, unlockWalletError }) diff --git a/app/reducers/onboarding.js b/app/reducers/onboarding.js index 68100e3a..341ab327 100644 --- a/app/reducers/onboarding.js +++ b/app/reducers/onboarding.js @@ -260,7 +260,7 @@ export const createWallet = () => dispatch => { dispatch({ type: CHANGE_STEP, step: 4 }) } -export const successfullyCreatedWallet = () => dispatch => dispatch({ type: ONBOARDING_FINISHED }) +export const finishOnboarding = () => dispatch => dispatch({ type: ONBOARDING_FINISHED }) // Listener for when LND creates and sends us a generated seed export const receiveSeed = (event, { cipher_seed_mnemonic }) => dispatch => { diff --git a/app/reducers/requestform.js b/app/reducers/requestform.js index 920be7ef..907610ef 100644 --- a/app/reducers/requestform.js +++ b/app/reducers/requestform.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect' -import { btc } from 'utils' +import { btc } from '../utils' import { tickerSelectors } from './ticker' // Initial State diff --git a/app/zap.js b/app/zap.js index fb7ebfb8..25fea67a 100644 --- a/app/zap.js +++ b/app/zap.js @@ -1,7 +1,9 @@ import { app, ipcMain, dialog } from 'electron' import Store from 'electron-store' +import StateMachine from 'javascript-state-machine' import lnd from './lnd' import Neutrino from './lnd/lib/neutrino' +import Lightning from './lnd/lib/lightning' import { mainLog } from './utils/log' import { isLndRunning } from './lnd/lib/util' @@ -37,7 +39,7 @@ const grpcSslCipherSuites = connectionType => class ZapController { /** * Create a new ZapController instance. - * @param {BrowserWindow} mainWindow BrowserWindow instance to interact with. + * @param {BrowserWindow} mainWindow BrowserWindow instance to interact with. */ constructor(mainWindow) { // Variable to hold the main window instance. @@ -46,14 +48,24 @@ class ZapController { // Keep a reference any neutrino process started by us. this.neutrino = null + // Keep a reference to the lightning gRPC instance. + this.lightning = null + // Time for the splash screen to remain visible. this.splashScreenTime = 500 + + // Boolean indicating wether the lightning grpc is connected ot not. + this.lightningGrpcConnected = false + + // Initialize the state machine. + this._fsm() } /** - * Initialize the application. + * Initialize the controller. */ init() { + // Load the application into the main window. if (process.env.HOT) { const port = process.env.PORT || 1212 this.mainWindow.loadURL(`http://localhost:${port}/dist/index.html`) @@ -68,51 +80,123 @@ class ZapController { this.mainWindow.webContents.on('did-finish-load', () => { this.mainWindow.show() this.mainWindow.focus() - mainLog.timeEnd('Time until app is visible') - return setTimeout(async () => { - this.sendMessage('startOnboarding') - }, this.splashScreenTime) + // Show the splash screen and then start onboarding. + setTimeout(() => this.startOnboarding(), this.splashScreenTime) }) - this.mainWindow.on('closed', () => { - this.mainWindow = null + // When the window is closed, just hide it unless we are force closing. + this.mainWindow.on('close', e => { + if (this.mainWindow.forceClose) { + return + } + e.preventDefault() + this.mainWindow.hide() + }) + } - // shut down zap when a user closes the window - app.quit() + // ------------------------------------ + // FSM Callbacks + // ------------------------------------ + + onStartOnboarding() { + mainLog.debug('[FSM] onStartOnboarding...') + this.sendMessage('startOnboarding') + } + + onStartLnd(options) { + mainLog.debug('[FSM] onStartLnd...') + return isLndRunning().then(res => { + if (res) { + mainLog.error('lnd already running: %s', res) + dialog.showMessageBox({ + type: 'error', + message: 'Unable to start lnd because it is already running.' + }) + return app.quit() + } + mainLog.info('Starting new lnd instance') + mainLog.info(' > alias:', options.alias) + mainLog.info(' > autopilot:', options.autopilot) + return this.startNeutrino(options.alias, options.autopilot) }) } + onConnectLnd(options) { + mainLog.debug('[FSM] onConnectLnd...') + mainLog.info('Connecting to custom lnd instance') + mainLog.info(' > host:', options.host) + mainLog.info(' > cert:', options.cert) + mainLog.info(' > macaroon:', options.macaroon) + this.startLightningWallet() + .then(() => this.sendMessage('finishOnboarding')) + .catch(e => { + const errors = {} + // There was a problem connectig to the host. + if (e.code === 'LND_GRPC_HOST_ERROR') { + errors.host = e.message + } + // There was a problem accessing loading the ssl cert. + if (e.code === 'LND_GRPC_CERT_ERROR') { + errors.cert = e.message + } + // There was a problem accessing loading the macaroon file. + else if (e.code === 'LND_GRPC_MACAROON_ERROR') { + errors.macaroon = e.message + } + // Other error codes such as UNAVAILABLE most likely indicate that there is a problem with the host. + else { + errors.host = `Unable to connect to host: ${e.details || e.message}` + } + + // Notify the app of errors. + return this.sendMessage('startLndError', errors) + }) + } + + onTerminated() { + mainLog.debug('[FSM] onTerminated...') + // Unsubscribe the gRPC streams before thhe window closes. This ensures that we can properly reestablish a fresh + // connection when a new window is opened. + this.disconnectLightningWallet() + + // If Neutrino is running, kill it. + if (this.neutrino) { + this.neutrino.stop() + } + + // Give the grpc connections a chance to be properly closed out. + return new Promise(resolve => setTimeout(resolve, 200)) + } + + onTerminate() { + mainLog.debug('[FSM] onTerminate...') + app.quit() + } + + // ------------------------------------ + // Helpers + // ------------------------------------ + /** * 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. - */ - async startLightningWallet() { - mainLog.info('Starting lightning wallet...') - const { lndSubscribe, lndMethods } = await 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('lightningGrpcActive') + if (this.mainWindow) { + mainLog.info('Sending message to renderer process: %o', { msg, data }) + this.mainWindow.webContents.send(msg, data) + } else { + mainLog.warn('Unable to send message to renderer process (main window not available): %o', { + msg, + data + }) + } } /** - * Create and subscribe to the WalletUnlocker grpc object. + * Start the wallet unlocker. */ startWalletUnlocker() { mainLog.info('Starting wallet unlocker...') @@ -124,6 +208,7 @@ class ZapController { walletUnlockerMethods(event, msg, data) }) + // Notify the renderer that the wallet unlocker is active. this.sendMessage('walletUnlockerGrpcActive') } catch (error) { dialog.showMessageBox({ @@ -134,13 +219,54 @@ class ZapController { } } + /** + * Create and subscribe to the Lightning service. + */ + async startLightningWallet() { + mainLog.info('Starting lightning wallet...') + this.lightning = new Lightning() + + // Connect to the Lightning interface. + await this.lightning.connect() + + // Subscribe the main window to receive streams. + this.lightning.subscribe(this.mainWindow) + + // Listen for all gRPC restful methods and pass to gRPC. + ipcMain.on('lnd', (event, { msg, data }) => this.lightning.lndMethods(event, msg, data)) + + // Let the renderer know that we are connected. + this.sendMessage('lightningGrpcActive') + + // Update our internal state. + this.lightningGrpcConnected = true + } + + /** + * Unsubscribe from the Lightning service. + */ + disconnectLightningWallet() { + if (!this.lightningGrpcConnected) { + return + } + mainLog.info('Disconnecting lightning Wallet...') + + // Disconnect streams. + this.lightning.unsubscribe() + + // Update our internal state. + this.lightningGrpcConnected = false + } + + /** /** * 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) { + startNeutrino(alias, autopilot) { + mainLog.info('Starting Neutrino...') this.neutrino = new Neutrino(alias, autopilot) this.neutrino.on('error', error => { @@ -153,7 +279,13 @@ class ZapController { this.neutrino.on('close', code => { mainLog.info(`Lnd process has shut down (code ${code})`) - app.quit() + if (['running', 'connected'].includes(this.state)) { + dialog.showMessageBox({ + type: 'error', + message: `Lnd has unexpectadly quit` + }) + this.terminate() + } }) this.neutrino.on('wallet-unlocker-grpc-active', () => { @@ -192,75 +324,43 @@ class ZapController { this.neutrino.start() } + finishOnboarding(options) { + // Trim any user supplied strings. + const cleanOptions = Object.keys(options).reduce((previous, current) => { + previous[current] = + typeof options[current] === 'string' ? options[current].trim() : options[current] + return previous + }, {}) + + // Save the options. + const store = new Store({ name: 'connection' }) + store.store = cleanOptions + mainLog.info('Saved lnd config to %s: %o', store.path, store.store) + + // Set up SSL with the cypher suits that we need based on the connection type. + process.env.GRPC_SSL_CIPHER_SUITES = + process.env.GRPC_SSL_CIPHER_SUITES || grpcSslCipherSuites(options.type) + + // If the requested connection type is a local one then start up a new lnd instance. + // // Otherwise attempt to connect to an lnd instance using user supplied connection details. + cleanOptions.type === 'local' ? this.startLnd() : this.connectLnd() + } + /** * Add IPC event listeners... */ _registerIpcListeners() { - ipcMain.on('startLnd', (event, options = {}) => { - // Trim any user supplied strings. - const cleanOptions = Object.keys(options).reduce((previous, current) => { - previous[current] = - typeof options[current] === 'string' ? options[current].trim() : options[current] - return previous - }, {}) - - // Save the options. - const store = new Store({ name: 'connection' }) - store.store = cleanOptions - mainLog.info('Saved lnd config to %s: %o', store.path, store.store) - - process.env.GRPC_SSL_CIPHER_SUITES = - process.env.GRPC_SSL_CIPHER_SUITES || grpcSslCipherSuites(options.type) - - // If the requested connection type is a local one then start up a new lnd instance. - if (cleanOptions.type === 'local') { - return isLndRunning().then(res => { - if (res) { - mainLog.error('lnd already running: %s', res) - dialog.showMessageBox({ - type: 'error', - message: 'Unable to start lnd because it is already running.' - }) - return app.quit() - } - mainLog.info('Starting new lnd instance') - mainLog.info(' > alias:', cleanOptions.alias) - mainLog.info(' > autopilot:', cleanOptions.autopilot) - return this.startLnd(cleanOptions.alias, cleanOptions.autopilot) - }) - } - - // Otherwise attempt to connect to an lnd instance using user supplied connection details. - mainLog.info('Connecting to custom lnd instance') - mainLog.info(' > host:', cleanOptions.host) - mainLog.info(' > cert:', cleanOptions.cert) - mainLog.info(' > macaroon:', cleanOptions.macaroon) - this.startLightningWallet() - .then(() => this.sendMessage('successfullyCreatedWallet')) - .catch(e => { - const errors = {} - // There was a problem connectig to the host. - if (e.code === 'LND_GRPC_HOST_ERROR') { - errors.host = e.message - } - // There was a problem accessing loading the ssl cert. - if (e.code === 'LND_GRPC_CERT_ERROR') { - errors.cert = e.message - } - // There was a problem accessing loading the macaroon file. - else if (e.code === 'LND_GRPC_MACAROON_ERROR') { - errors.macaroon = e.message - } - // Other error codes such as UNAVAILABLE most likely indicate that there is a problem with the host. - else { - errors.host = `Unable to connect to host: ${e.details || e.message}` - } - - // Notify the app of errors. - return this.sendMessage('startLndError', errors) - }) - }) + ipcMain.on('startLnd', (event, options = {}) => this.finishOnboarding(options)) } } +StateMachine.factory(ZapController, { + transitions: [ + { name: 'startOnboarding', from: 'none', to: 'onboarding' }, + { name: 'startLnd', from: 'onboarding', to: 'running' }, + { name: 'connectLnd', from: 'onboarding', to: 'connected' }, + { name: 'terminate', from: '*', to: 'terminated' } + ] +}) + export default ZapController diff --git a/package.json b/package.json index 8b4ffd71..5e63c18c 100644 --- a/package.json +++ b/package.json @@ -282,6 +282,7 @@ "electron-store": "^2.0.0", "font-awesome": "^4.7.0", "history": "^4.6.3", + "javascript-state-machine": "^3.1.0", "lodash.get": "^4.4.2", "moment": "^2.22.2", "prop-types": "^15.5.10", diff --git a/yarn.lock b/yarn.lock index d8d0f299..ec4ff411 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3142,6 +3142,10 @@ connect-history-api-fallback@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" +connected-domain@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/connected-domain/-/connected-domain-1.0.0.tgz#bfe77238c74be453a79f0cb6058deeb4f2358e93" + console-browserify@1.1.x, console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" @@ -6630,6 +6634,10 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" +javascript-state-machine@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/javascript-state-machine/-/javascript-state-machine-3.1.0.tgz#06eeb2136a6a19ae1b56105c25caec283dd5cd14" + jest-changed-files@^23.0.1: version "23.0.1" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.0.1.tgz#f79572d0720844ea5df84c2a448e862c2254f60c" @@ -9587,6 +9595,12 @@ prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" +ps-node@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/ps-node/-/ps-node-0.1.6.tgz#9af67a99d7b1d0132e51a503099d38a8d2ace2c3" + dependencies: + table-parser "^0.1.3" + pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -11453,6 +11467,12 @@ symbol-tree@^3.2.1, symbol-tree@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" +table-parser@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/table-parser/-/table-parser-0.1.3.tgz#0441cfce16a59481684c27d1b5a67ff15a43c7b0" + dependencies: + connected-domain "^1.0.0" + table@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/table/-/table-4.0.1.tgz#a8116c133fac2c61f4a420ab6cdf5c4d61f0e435"