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