diff --git a/app/containers/Root.js b/app/containers/Root.js
index ebedb68d..bd9018d9 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 0f0e7600..cd1cc757 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(5), 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/neutrino.js b/app/lib/lnd/neutrino.js
index 23914b07..dd2c55f3 100644
--- a/app/lib/lnd/neutrino.js
+++ b/app/lib/lnd/neutrino.js
@@ -187,6 +187,8 @@ class Neutrino extends EventEmitter {
height = match[1]
} else if ((match = line.match(/Processed \d* blocks? in the last.+\(height (\d+)/))) {
height = match[1]
+ } else if ((match = line.match(/Difficulty retarget at block height (\d+)/))) {
+ height = match[1]
} else if ((match = line.match(/Fetching set of headers from tip \(height=(\d+)/))) {
height = match[1]
} else if ((match = line.match(/Waiting for filter headers \(height=(\d+)\) to catch/))) {
@@ -201,6 +203,8 @@ class Neutrino extends EventEmitter {
cfilter = match[1]
} else if ((match = line.match(/Verified \d* filter headers? in the.+\(height (\d+)/))) {
cfilter = match[1]
+ } else if ((match = line.match(/Fetching filter for height=(\d+)/))) {
+ cfilter = match[1]
}
if (height) {
@@ -285,8 +289,11 @@ class Neutrino extends EventEmitter {
*/
setLndCfilterHeight(height: number | string) {
const heightAsNumber = Number(height)
- this.lndCfilterHeight = heightAsNumber
- this.emit(GOT_LND_CFILTER_HEIGHT, heightAsNumber)
+ const changed = Neutrino.incrementIfHigher(this, 'lndCfilterHeight', heightAsNumber)
+ if (changed) {
+ this.emit(GOT_LND_CFILTER_HEIGHT, heightAsNumber)
+ this.setCurrentBlockHeight(heightAsNumber)
+ }
}
}
diff --git a/app/lib/lnd/util.js b/app/lib/lnd/util.js
index 2b6743f5..f08616c7 100644
--- a/app/lib/lnd/util.js
+++ b/app/lib/lnd/util.js
@@ -165,19 +165,56 @@ export const createSslCreds = async certPath => {
export const createMacaroonCreds = async macaroonPath => {
const metadata = new grpc.Metadata()
- // If it's not a filepath, then assume it is a hex encoded string.
- if (macaroonPath === basename(macaroonPath)) {
- metadata.add('macaroon', macaroonPath)
- } else {
- const macaroon = await fsReadFile(macaroonPath).catch(e => {
- const error = new Error(`Macaroon path could not be accessed: ${e.message}`)
- error.code = 'LND_GRPC_MACAROON_ERROR'
- throw error
- })
- metadata.add('macaroon', macaroon.toString('hex'))
+ if (macaroonPath) {
+ // If it's not a filepath, then assume it is a hex encoded string.
+ if (macaroonPath === basename(macaroonPath)) {
+ metadata.add('macaroon', macaroonPath)
+ } else {
+ const macaroon = await fsReadFile(macaroonPath).catch(e => {
+ const error = new Error(`Macaroon path could not be accessed: ${e.message}`)
+ error.code = 'LND_GRPC_MACAROON_ERROR'
+ throw error
+ })
+ metadata.add('macaroon', macaroon.toString('hex'))
+ }
}
-
return grpc.credentials.createFromMetadataGenerator((params, callback) =>
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 d833a19d..c531e49e 100644
--- a/app/lib/lnd/walletUnlocker.js
+++ b/app/lib/lnd/walletUnlocker.js
@@ -4,7 +4,7 @@ import grpc from 'grpc'
import { loadSync } from '@grpc/proto-loader'
import StateMachine from 'javascript-state-machine'
import LndConfig from './config'
-import { getDeadline, validateHost, createSslCreds, createMacaroonCreds } from './util'
+import { getDeadline, validateHost, createSslCreds } from './util'
import methods from './walletUnlockerMethods'
import { mainLog } from '../utils/log'
@@ -43,7 +43,7 @@ class WalletUnlocker {
*/
async onBeforeConnect() {
mainLog.info('Connecting to WalletUnlocker gRPC service')
- const { rpcProtoPath, host, cert, macaroon } = this.lndConfig
+ const { rpcProtoPath, host, cert } = this.lndConfig
// Verify that the host is valid before creating a gRPC client that is connected to it.
return await validateHost(host).then(async () => {
@@ -62,21 +62,19 @@ class WalletUnlocker {
// 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 ssl credentials to use with the gRPC client.
+ const sslCreds = await createSslCreds(cert)
// Create a new gRPC client instance.
- this.service = new rpc.lnrpc.WalletUnlocker(host, credentials)
+ this.service = new rpc.lnrpc.WalletUnlocker(host, sslCreds)
// Wait for the gRPC connection to be established.
return new Promise((resolve, reject) => {
- this.service.waitForReady(getDeadline(5), 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 bfd9f6a2..19d2f56b 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 4bec4c10..8f1255ce 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 77f9d157..5977109e 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 7dcbdd48..9e7dfd6e 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 e9fd1a1e..b42dfa4e 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 })
}
diff --git a/package.json b/package.json
index e1b48031..f7907494 100644
--- a/package.json
+++ b/package.json
@@ -42,7 +42,7 @@
"config": {
"style_paths": "app/styles/*.scss app/components/**/*.scss",
"lnd-binary": {
- "binaryVersion": "0.5-beta-rc1-52-gbaee07ef",
+ "binaryVersion": "0.5-beta-rc2-41-g4dd4f7cf",
"binarySite": "https://github.com/LN-Zap/lnd/releases/download"
}
},