Ben Woosley
7 years ago
committed by
GitHub
6 changed files with 414 additions and 361 deletions
@ -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 |
@ -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 |
@ -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 |
Loading…
Reference in new issue