Skip to content
This repository has been archived by the owner on Nov 17, 2023. It is now read-only.

Commit

Permalink
fix(grpc): ensure connection to lnd on init
Browse files Browse the repository at this point in the history
Consolidate the flow for local and remote wallet connections for
consolidate. When unlocking a local wallet wait for the macaroon to be
generated before we try to use it.
  • Loading branch information
mrfelton committed Sep 12, 2018
1 parent e47eacf commit 42a956a
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 68 deletions.
4 changes: 2 additions & 2 deletions app/containers/Root.js
Expand Up @@ -225,8 +225,8 @@ const Root = ({
return <Syncing {...syncingProps} />
}

// 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 <LoadingBolt />
}

Expand Down
92 changes: 50 additions & 42 deletions app/lib/lnd/lightning.js
Expand Up @@ -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'
Expand Down Expand Up @@ -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
})
)
}

/**
Expand Down
36 changes: 36 additions & 0 deletions app/lib/lnd/util.js
Expand Up @@ -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])
}
6 changes: 4 additions & 2 deletions app/lib/lnd/walletUnlocker.js
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion app/lib/lnd/walletUnlockerMethods/index.js
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion app/lib/zap/controller.js
Expand Up @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions app/reducers/ipc.js
Expand Up @@ -4,7 +4,8 @@ import {
currentBlockHeight,
lndBlockHeight,
lndCfilterHeight,
lightningGrpcActive
lightningGrpcActive,
walletUnlockerGrpcActive
} from './lnd'
import { receiveInfo } from './info'
import { receiveAddress } from './address'
Expand Down Expand Up @@ -47,11 +48,11 @@ import { receiveDescribeNetwork, receiveQueryRoutes, receiveInvoiceAndQueryRoute
import {
startOnboarding,
startLndError,
walletUnlockerGrpcActive,
receiveSeed,
receiveSeedError,
finishOnboarding,
walletCreated,
walletUnlocked,
walletConnected,
unlockWalletError
} from './onboarding'

Expand Down Expand Up @@ -118,8 +119,9 @@ const ipc = createIpc({
walletUnlockerGrpcActive,
receiveSeed,
receiveSeedError,
finishOnboarding,
walletCreated,
walletUnlocked,
walletConnected,
unlockWalletError
})

Expand Down
28 changes: 26 additions & 2 deletions app/reducers/lnd.js
Expand Up @@ -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
// ------------------------------------
Expand All @@ -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'

// ------------------------------------
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -96,14 +110,24 @@ 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
})
}

// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
syncStatus: 'pending',
walletUnlockerGrpcActive: false,
lightningGrpcActive: false,
blockHeight: 0,
lndBlockHeight: 0,
Expand Down
58 changes: 44 additions & 14 deletions 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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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 })
}
Expand Down

0 comments on commit 42a956a

Please sign in to comment.