Browse Source

Merge pull request #751 from mrfelton/fix/grpc-flow

fix(grpc): ensure connection to lnd on init
renovate/lint-staged-8.x
JimmyMow 6 years ago
committed by GitHub
parent
commit
fb565e3e4e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/containers/Root.js
  2. 92
      app/lib/lnd/lightning.js
  3. 11
      app/lib/lnd/neutrino.js
  4. 59
      app/lib/lnd/util.js
  5. 20
      app/lib/lnd/walletUnlocker.js
  6. 2
      app/lib/lnd/walletUnlockerMethods/index.js
  7. 2
      app/lib/zap/controller.js
  8. 10
      app/reducers/ipc.js
  9. 28
      app/reducers/lnd.js
  10. 58
      app/reducers/onboarding.js
  11. 2
      package.json

4
app/containers/Root.js

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

92
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
})
)
}
/**

11
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)
}
}
}

59
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])
}

20
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()

2
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:

2
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.

10
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
})

28
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,

58
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 })
}

2
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"
}
},

Loading…
Cancel
Save