diff --git a/app/containers/Root.js b/app/containers/Root.js index ebedb68d5cb..bd9018d9897 100644 --- a/app/containers/Root.js +++ b/app/containers/Root.js @@ -225,8 +225,8 @@ const Root = ({ return } - // Don't launch the app without a connection to the lightning wallet gRPC interface. - if (!lnd.lightningGrpcActive) { + // Don't launch the app without a connection to lnd. + if (!lnd.lightningGrpcActive && !lnd.walletUnlockerGrpcActive) { return } diff --git a/app/lib/lnd/lightning.js b/app/lib/lnd/lightning.js index 97c735b8d34..cd1cc7579d6 100644 --- a/app/lib/lnd/lightning.js +++ b/app/lib/lnd/lightning.js @@ -5,7 +5,7 @@ import { loadSync } from '@grpc/proto-loader' import { BrowserWindow } from 'electron' import StateMachine from 'javascript-state-machine' import LndConfig from './config' -import { getDeadline, validateHost, createSslCreds, createMacaroonCreds } from './util' +import { getDeadline, validateHost, createSslCreds, createMacaroonCreds, waitForFile } from './util' import methods from './methods' import { mainLog } from '../utils/log' import subscribeToTransactions from './subscribe/transactions' @@ -63,51 +63,59 @@ class Lightning { */ async onBeforeConnect() { mainLog.info('Connecting to Lightning gRPC service') - const { rpcProtoPath, host, cert, macaroon } = this.lndConfig + const { rpcProtoPath, host, cert, macaroon, type } = this.lndConfig // Verify that the host is valid before creating a gRPC client that is connected to it. - return 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: Number, - 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. - this.service = new rpc.lnrpc.Lightning(host, credentials) - - // Wait for the gRPC connection to be established. - return new Promise((resolve, reject) => { - this.service.waitForReady(getDeadline(10), err => { - if (err) { - return reject(err) - } - return resolve() + return ( + validateHost(host) + // If we are trying to connect to the internal lnd, wait upto 20 seconds for the macaroon to be generated. + .then(() => (type === 'local' ? waitForFile(macaroon, 20000) : Promise.resolve())) + // Attempt to connect using the supplied credentials. + .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: Number, + 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. + this.service = new rpc.lnrpc.Lightning(host, credentials) + + // Wait upto 20 seconds for the gRPC connection to be established. + return new Promise((resolve, reject) => { + this.service.waitForReady(getDeadline(20), err => { + if (err) { + return reject(err) + } + return resolve() + }) }) }) - }) - .then(() => getInfo(this.service)) - .catch(err => { - this.service.close() - throw err - }) + // Once connected, make a call to getInfo to verify that we can make successful calls. + .then(() => getInfo(this.service)) + .catch(err => { + if (this.service) { + this.service.close() + } + throw err + }) + ) } /** diff --git a/app/lib/lnd/util.js b/app/lib/lnd/util.js index 51830ac6812..f08616c71c4 100644 --- a/app/lib/lnd/util.js +++ b/app/lib/lnd/util.js @@ -182,3 +182,39 @@ export const createMacaroonCreds = async macaroonPath => { callback(null, metadata) ) } + +/** + * Wait for a file to exist. + * @param {String} filepath + */ +export const waitForFile = (filepath, timeout = 1000) => { + let timeoutId + let intervalId + + // Promise A rejects after the timeout has passed. + let promiseA = new Promise((resolve, reject) => { + timeoutId = setTimeout(() => { + mainLog.debug('deadline (%sms) exceeded before file (%s) was found', timeout, filepath) + clearInterval(intervalId) + clearTimeout(timeoutId) + reject(new Error(`Unable to find file: ${filepath}`)) + }, timeout) + }) + + // Promise B resolves when the file has been found. + let promiseB = new Promise(resolve => { + let intervalId = setInterval(() => { + mainLog.debug('waiting for file: %s', filepath) + if (!fs.existsSync(filepath)) { + return + } + mainLog.debug('found file: %s', filepath) + clearInterval(intervalId) + clearTimeout(timeoutId) + resolve() + }, 200) + }) + + // Let's race our promises. + return Promise.race([promiseA, promiseB]) +} diff --git a/app/lib/lnd/walletUnlocker.js b/app/lib/lnd/walletUnlocker.js index c69b72de38d..c531e49e838 100644 --- a/app/lib/lnd/walletUnlocker.js +++ b/app/lib/lnd/walletUnlocker.js @@ -70,9 +70,11 @@ class WalletUnlocker { // Wait for the gRPC connection to be established. return new Promise((resolve, reject) => { - this.service.waitForReady(getDeadline(10), err => { + this.service.waitForReady(getDeadline(20), err => { if (err) { - this.service.close() + if (this.service) { + this.service.close() + } return reject(err) } return resolve() diff --git a/app/lib/lnd/walletUnlockerMethods/index.js b/app/lib/lnd/walletUnlockerMethods/index.js index bfd9f6a2853..19d2f56b0c4 100644 --- a/app/lib/lnd/walletUnlockerMethods/index.js +++ b/app/lib/lnd/walletUnlockerMethods/index.js @@ -30,7 +30,7 @@ export default function(walletUnlocker, log, event, msg, data, lndConfig) { case 'initWallet': walletController .initWallet(walletUnlocker, data) - .then(() => event.sender.send('finishOnboarding')) + .then(() => event.sender.send('walletCreated')) .catch(error => log.error('initWallet:', error)) break default: diff --git a/app/lib/zap/controller.js b/app/lib/zap/controller.js index 4bec4c10d77..8f1255ce8d5 100644 --- a/app/lib/zap/controller.js +++ b/app/lib/zap/controller.js @@ -166,7 +166,7 @@ class ZapController { mainLog.info(' > macaroon:', this.lndConfig.macaroon) return this.startLightningWallet() - .then(() => this.sendMessage('finishOnboarding')) + .then(() => this.sendMessage('walletConnected')) .catch(e => { const errors = {} // There was a problem connectig to the host. diff --git a/app/reducers/ipc.js b/app/reducers/ipc.js index 77f9d1577f5..5977109e868 100644 --- a/app/reducers/ipc.js +++ b/app/reducers/ipc.js @@ -4,7 +4,8 @@ import { currentBlockHeight, lndBlockHeight, lndCfilterHeight, - lightningGrpcActive + lightningGrpcActive, + walletUnlockerGrpcActive } from './lnd' import { receiveInfo } from './info' import { receiveAddress } from './address' @@ -47,11 +48,11 @@ import { receiveDescribeNetwork, receiveQueryRoutes, receiveInvoiceAndQueryRoute import { startOnboarding, startLndError, - walletUnlockerGrpcActive, receiveSeed, receiveSeedError, - finishOnboarding, + walletCreated, walletUnlocked, + walletConnected, unlockWalletError } from './onboarding' @@ -118,8 +119,9 @@ const ipc = createIpc({ walletUnlockerGrpcActive, receiveSeed, receiveSeedError, - finishOnboarding, + walletCreated, walletUnlocked, + walletConnected, unlockWalletError }) diff --git a/app/reducers/lnd.js b/app/reducers/lnd.js index 7dcbdd4820e..9e7dfd6e832 100644 --- a/app/reducers/lnd.js +++ b/app/reducers/lnd.js @@ -4,6 +4,8 @@ import { showNotification } from 'lib/utils/notifications' import { fetchTicker } from './ticker' import { fetchBalance } from './balance' import { fetchInfo, setHasSynced } from './info' +import { lndWalletStarted, lndWalletUnlockerStarted } from './onboarding' + // ------------------------------------ // Constants // ------------------------------------ @@ -16,6 +18,7 @@ export const RECEIVE_CURRENT_BLOCK_HEIGHT = 'RECEIVE_CURRENT_BLOCK_HEIGHT' export const RECEIVE_LND_BLOCK_HEIGHT = 'RECEIVE_LND_BLOCK_HEIGHT' export const RECEIVE_LND_CFILTER_HEIGHT = 'RECEIVE_LND_CFILTER_HEIGHT' +export const SET_WALLET_UNLOCKER_ACTIVE = 'SET_WALLET_UNLOCKER_ACTIVE' export const SET_LIGHTNING_WALLET_ACTIVE = 'SET_LIGHTNING_WALLET_ACTIVE' // ------------------------------------ @@ -60,9 +63,20 @@ export const lndSyncStatus = (event, status) => (dispatch, getState) => { } } +// Connected to Lightning gRPC interface (lnd wallet is connected and unlocked) export const lightningGrpcActive = () => dispatch => { - dispatch(fetchInfo()) dispatch({ type: SET_LIGHTNING_WALLET_ACTIVE }) + + // Let the onboarding process know that wallet is active. + dispatch(lndWalletStarted()) +} + +// Connected to WalletUnlocker gRPC interface (lnd is ready to unlock or create wallet) +export const walletUnlockerGrpcActive = () => dispatch => { + dispatch({ type: SET_WALLET_UNLOCKER_ACTIVE }) + + // Let the onboarding process know that the wallet unlocker has started. + dispatch(lndWalletUnlockerStarted()) } // Receive IPC event for current height. @@ -96,7 +110,16 @@ const ACTION_HANDLERS = { [RECEIVE_LND_BLOCK_HEIGHT]: (state, { lndBlockHeight }) => ({ ...state, lndBlockHeight }), [RECEIVE_LND_CFILTER_HEIGHT]: (state, { lndCfilterHeight }) => ({ ...state, lndCfilterHeight }), - [SET_LIGHTNING_WALLET_ACTIVE]: state => ({ ...state, lightningGrpcActive: true }) + [SET_WALLET_UNLOCKER_ACTIVE]: state => ({ + ...state, + walletUnlockerGrpcActive: true, + lightningGrpcActive: false + }), + [SET_LIGHTNING_WALLET_ACTIVE]: state => ({ + ...state, + lightningGrpcActive: true, + walletUnlockerGrpcActive: false + }) } // ------------------------------------ @@ -104,6 +127,7 @@ const ACTION_HANDLERS = { // ------------------------------------ const initialState = { syncStatus: 'pending', + walletUnlockerGrpcActive: false, lightningGrpcActive: false, blockHeight: 0, lndBlockHeight: 0, diff --git a/app/reducers/onboarding.js b/app/reducers/onboarding.js index e9fd1a1eb17..b42dfa4e348 100644 --- a/app/reducers/onboarding.js +++ b/app/reducers/onboarding.js @@ -1,6 +1,7 @@ import { createSelector } from 'reselect' import { ipcRenderer } from 'electron' import get from 'lodash.get' +import { fetchInfo } from './info' // ------------------------------------ // Constants @@ -178,13 +179,6 @@ export function changeStep(step) { } } -export function setStartLndError(errors) { - return { - type: SET_START_LND_ERROR, - errors - } -} - export function startLnd(options) { // once the user submits the data needed to start LND we will alert the app that it should start LND ipcRenderer.send('startLnd', options) @@ -194,6 +188,19 @@ export function startLnd(options) { } } +export function lndStarted() { + return { + type: LND_STARTED + } +} + +export function setStartLndError(errors) { + return { + type: SET_START_LND_ERROR, + errors + } +} + export function setReEnterSeedIndexes() { // we only want the user to have to verify 3 random indexes from the seed they were just given const INDEX_AMOUNT = 3 @@ -216,6 +223,25 @@ export function setReEnterSeedIndexes() { } } +/** + * As soon as we have an active connection to a WalletUnlocker service, attempt to generate a new seed which kicks off + * the process of creating or unlocking a wallet. + */ +export const lndWalletUnlockerStarted = () => dispatch => { + dispatch(lndStarted()) + ipcRenderer.send('walletUnlocker', { msg: 'genSeed' }) + dispatch({ type: FETCH_SEED }) +} + +/** + * As soon as we have an active connection to an unlocked wallet, fetch the wallet info so that we have the key data as + * early as possible. + */ +export const lndWalletStarted = () => dispatch => { + dispatch(lndStarted()) + dispatch(fetchInfo()) +} + export const submitNewWallet = ( wallet_password, cipher_seed_mnemonic, @@ -279,13 +305,6 @@ export const startLndError = (event, errors) => (dispatch, getState) => { } } -// Listener from after the LND walletUnlocker has started -export const walletUnlockerGrpcActive = () => dispatch => { - dispatch({ type: LND_STARTED }) - ipcRenderer.send('walletUnlocker', { msg: 'genSeed' }) - dispatch({ type: FETCH_SEED }) -} - export const createWallet = () => dispatch => { ipcRenderer.send('walletUnlocker', { msg: 'genSeed' }) dispatch({ type: CHANGE_STEP, step: 4 }) @@ -317,12 +336,23 @@ export const unlockWallet = wallet_password => dispatch => { dispatch({ type: UNLOCKING_WALLET }) } +export const walletCreated = () => dispatch => { + dispatch({ type: WALLET_UNLOCKED }) + dispatch({ type: ONBOARDING_FINISHED }) + ipcRenderer.send('startLightningWallet') +} + export const walletUnlocked = () => dispatch => { dispatch({ type: WALLET_UNLOCKED }) dispatch({ type: ONBOARDING_FINISHED }) ipcRenderer.send('startLightningWallet') } +export const walletConnected = () => dispatch => { + dispatch({ type: WALLET_UNLOCKED }) + dispatch({ type: ONBOARDING_FINISHED }) +} + export const unlockWalletError = () => dispatch => { dispatch({ type: SET_UNLOCK_WALLET_ERROR }) }