|
|
@ -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 |
|
|
|