Browse Source

Merge pull request #741 from mrfelton/fix/detect-remote-wallet-locked

fix(grpc): show error on connect to locked wallet
renovate/lint-staged-8.x
JimmyMow 7 years ago
committed by GitHub
parent
commit
0a6953343c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 89
      app/lib/lnd/lightning.js
  2. 2
      app/lib/lnd/methods/index.js
  3. 2
      app/lib/lnd/subscribe/channelgraph.js
  4. 2
      app/lib/lnd/subscribe/invoices.js
  5. 2
      app/lib/lnd/subscribe/transactions.js
  6. 134
      app/lib/lnd/walletUnlocker.js
  7. 2
      app/lib/lnd/walletUnlockerMethods/index.js
  8. 44
      app/lib/zap/controller.js
  9. 2
      app/reducers/info.js
  10. 2
      test/unit/lnd/lightning.spec.js

89
app/lib/lnd/lightning.js

@ -11,6 +11,7 @@ import { mainLog } from '../utils/log'
import subscribeToTransactions from './subscribe/transactions' import subscribeToTransactions from './subscribe/transactions'
import subscribeToInvoices from './subscribe/invoices' import subscribeToInvoices from './subscribe/invoices'
import subscribeToChannelGraph from './subscribe/channelgraph' import subscribeToChannelGraph from './subscribe/channelgraph'
import { getInfo } from './methods/networkController'
// Type definition for subscriptions property. // Type definition for subscriptions property.
type LightningSubscriptionsType = { type LightningSubscriptionsType = {
@ -25,7 +26,7 @@ type LightningSubscriptionsType = {
*/ */
class Lightning { class Lightning {
mainWindow: BrowserWindow mainWindow: BrowserWindow
lnd: any service: any
lndConfig: LndConfig lndConfig: LndConfig
subscriptions: LightningSubscriptionsType subscriptions: LightningSubscriptionsType
_fsm: StateMachine _fsm: StateMachine
@ -40,7 +41,7 @@ class Lightning {
constructor(lndConfig: LndConfig) { constructor(lndConfig: LndConfig) {
this.mainWindow = null this.mainWindow = null
this.lnd = null this.service = null
this.lndConfig = lndConfig this.lndConfig = lndConfig
this.subscriptions = { this.subscriptions = {
channelGraph: null, channelGraph: null,
@ -65,42 +66,48 @@ class Lightning {
const { rpcProtoPath, host, cert, macaroon } = this.lndConfig const { rpcProtoPath, host, cert, macaroon } = this.lndConfig
// Verify that the host is valid before creating a gRPC client that is connected to it. // Verify that the host is valid before creating a gRPC client that is connected to it.
return await validateHost(host).then(async () => { return validateHost(host)
// Load the gRPC proto file. .then(async () => {
// The following options object closely approximates the existing behavior of grpc.load. // Load the gRPC proto file.
// See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md // The following options object closely approximates the existing behavior of grpc.load.
const options = { // See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md
keepCase: true, const options = {
longs: Number, keepCase: true,
enums: String, longs: Number,
defaults: true, enums: String,
oneofs: true defaults: true,
} oneofs: true
const packageDefinition = loadSync(rpcProtoPath, options) }
const packageDefinition = loadSync(rpcProtoPath, options)
// Load gRPC package definition as a gRPC object hierarchy.
const rpc = grpc.loadPackageDefinition(packageDefinition) // 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([ // Create ssl and macaroon credentials to use with the gRPC client.
createSslCreds(cert), const [sslCreds, macaroonCreds] = await Promise.all([
createMacaroonCreds(macaroon) createSslCreds(cert),
]) createMacaroonCreds(macaroon)
const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds) ])
const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds)
// Create a new gRPC client instance.
this.lnd = new rpc.lnrpc.Lightning(host, credentials) // 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) => { // Wait for the gRPC connection to be established.
grpc.waitForClientReady(this.lnd, getDeadline(2), err => { return new Promise((resolve, reject) => {
if (err) { this.service.waitForReady(getDeadline(5), err => {
return reject(err) if (err) {
} return reject(err)
return resolve() }
return resolve()
})
}) })
}) })
}) .then(() => getInfo(this.service))
.catch(err => {
this.service.close()
throw err
})
} }
/** /**
@ -109,8 +116,8 @@ class Lightning {
onBeforeDisconnect() { onBeforeDisconnect() {
mainLog.info('Disconnecting from Lightning gRPC service') mainLog.info('Disconnecting from Lightning gRPC service')
this.unsubscribe() this.unsubscribe()
if (this.lnd) { if (this.service) {
this.lnd.close() this.service.close()
} }
} }
@ -121,7 +128,7 @@ class Lightning {
mainLog.info('Shutting down Lightning daemon') mainLog.info('Shutting down Lightning daemon')
this.unsubscribe() this.unsubscribe()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.lnd.stopDaemon({}, (err, data) => { this.service.stopDaemon({}, (err, data) => {
if (err) { if (err) {
return reject(err) return reject(err)
} }
@ -137,8 +144,8 @@ class Lightning {
/** /**
* Hook up lnd restful methods. * Hook up lnd restful methods.
*/ */
lndMethods(event: Event, msg: string, data: any) { registerMethods(event: Event, msg: string, data: any) {
return methods(this.lnd, mainLog, event, msg, data) return methods(this.service, mainLog, event, msg, data)
} }
/** /**

2
app/lib/lnd/methods/index.js

@ -28,7 +28,7 @@ export default function(lnd, log, event, msg, data) {
event.sender.send('receiveCryptocurrency', infoData.chains[0]) event.sender.send('receiveCryptocurrency', infoData.chains[0])
return infoData return infoData
}) })
.catch(() => event.sender.send('infoFailed')) .catch(error => log.error('info:', error))
break break
case 'describeNetwork': case 'describeNetwork':
networkController networkController

2
app/lib/lnd/subscribe/channelgraph.js

@ -2,7 +2,7 @@ import { status } from 'grpc'
import { mainLog } from '../../utils/log' import { mainLog } from '../../utils/log'
export default function subscribeToChannelGraph() { export default function subscribeToChannelGraph() {
const call = this.lnd.subscribeChannelGraph({}) const call = this.service.subscribeChannelGraph({})
call.on('data', channelGraphData => { call.on('data', channelGraphData => {
mainLog.info('CHANNELGRAPH:', channelGraphData) mainLog.info('CHANNELGRAPH:', channelGraphData)

2
app/lib/lnd/subscribe/invoices.js

@ -2,7 +2,7 @@ import { status } from 'grpc'
import { mainLog } from '../../utils/log' import { mainLog } from '../../utils/log'
export default function subscribeToInvoices() { export default function subscribeToInvoices() {
const call = this.lnd.subscribeInvoices({}) const call = this.service.subscribeInvoices({})
call.on('data', invoice => { call.on('data', invoice => {
mainLog.info('INVOICE:', invoice) mainLog.info('INVOICE:', invoice)

2
app/lib/lnd/subscribe/transactions.js

@ -2,7 +2,7 @@ import { status } from 'grpc'
import { mainLog } from '../../utils/log' import { mainLog } from '../../utils/log'
export default function subscribeToTransactions() { export default function subscribeToTransactions() {
const call = this.lnd.subscribeTransactions({}) const call = this.service.subscribeTransactions({})
call.on('data', transaction => { call.on('data', transaction => {
mainLog.info('TRANSACTION:', transaction) mainLog.info('TRANSACTION:', transaction)

134
app/lib/lnd/walletUnlocker.js

@ -1,36 +1,118 @@
import fs from 'fs' // @flow
import grpc from 'grpc' import grpc from 'grpc'
import { loadSync } from '@grpc/proto-loader' import { loadSync } from '@grpc/proto-loader'
import walletUnlockerMethods from './walletUnlockerMethods' import StateMachine from 'javascript-state-machine'
import LndConfig from './config'
import { getDeadline, validateHost, createSslCreds, createMacaroonCreds } from './util'
import methods from './walletUnlockerMethods'
import { mainLog } from '../utils/log' import { mainLog } from '../utils/log'
export const initWalletUnlocker = lndConfig => { /**
const walletUnlockerObj = walletUnlocker(lndConfig) * Creates an LND grpc client lightning service.
const walletUnlockerMethodsCallback = (event, msg, data) => * @returns {WalletUnlocker}
walletUnlockerMethods(lndConfig, walletUnlockerObj, mainLog, event, msg, data) */
class WalletUnlocker {
service: any
lndConfig: LndConfig
_fsm: StateMachine
return walletUnlockerMethodsCallback // Transitions provided by the state machine.
} connect: any
disconnect: any
terminate: any
is: any
can: any
state: string
constructor(lndConfig: LndConfig) {
this.service = null
this.lndConfig = lndConfig
// Initialize the state machine.
this._fsm()
}
// ------------------------------------
// FSM Callbacks
// ------------------------------------
/**
* Connect to the gRPC interface and verify it is functional.
* @return {Promise<rpc.lnrpc.WalletUnlocker>}
*/
async onBeforeConnect() {
mainLog.info('Connecting to WalletUnlocker gRPC service')
const { rpcProtoPath, host, cert, macaroon } = this.lndConfig
// Verify that the host is valid before creating a gRPC client that is connected to it.
return await 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)
export const walletUnlocker = lndConfig => { // Load gRPC package definition as a gRPC object hierarchy.
const lndCert = fs.readFileSync(lndConfig.cert) const rpc = grpc.loadPackageDefinition(packageDefinition)
const credentials = grpc.credentials.createSsl(lndCert)
// Create ssl and macaroon credentials to use with the gRPC client.
// Load the gRPC proto file. const [sslCreds, macaroonCreds] = await Promise.all([
// The following options object closely approximates the existing behavior of grpc.load createSslCreds(cert),
// See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md createMacaroonCreds(macaroon)
const options = { ])
keepCase: true, const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds)
longs: Number,
enums: String, // Create a new gRPC client instance.
defaults: true, this.service = new rpc.lnrpc.WalletUnlocker(host, credentials)
oneofs: true
// Wait for the gRPC connection to be established.
return new Promise((resolve, reject) => {
this.service.waitForReady(getDeadline(5), err => {
if (err) {
this.service.close()
return reject(err)
}
return resolve()
})
})
})
}
/**
* Discomnnect the gRPC service.
*/
onBeforeDisconnect() {
mainLog.info('Disconnecting from WalletUnlocker gRPC service')
if (this.service) {
this.service.close()
}
} }
const packageDefinition = loadSync(lndConfig.rpcProtoPath, options)
// Load gRPC package definition as a gRPC object hierarchy. // ------------------------------------
const rpc = grpc.loadPackageDefinition(packageDefinition) // Helpers
// ------------------------------------
// Instantiate a new connection to the WalletUnlocker interface. /**
return new rpc.lnrpc.WalletUnlocker(lndConfig.host, credentials) * Hook up lnd restful methods.
*/
registerMethods(event: Event, msg: string, data: any) {
return methods(this.service, mainLog, event, msg, data, this.lndConfig)
}
} }
StateMachine.factory(WalletUnlocker, {
init: 'ready',
transitions: [
{ name: 'connect', from: 'ready', to: 'connected' },
{ name: 'disconnect', from: 'connected', to: 'ready' }
]
})
export default WalletUnlocker

2
app/lib/lnd/walletUnlockerMethods/index.js

@ -1,7 +1,7 @@
import { dirname } from 'path' import { dirname } from 'path'
import * as walletController from '../methods/walletController' import * as walletController from '../methods/walletController'
export default function(lndConfig, walletUnlocker, log, event, msg, data) { export default function(walletUnlocker, log, event, msg, data, lndConfig) {
const decorateError = error => { const decorateError = error => {
switch (error.code) { switch (error.code) {
// wallet already exists // wallet already exists

44
app/lib/zap/controller.js

@ -4,12 +4,13 @@ import { app, ipcMain, dialog, BrowserWindow } from 'electron'
import pick from 'lodash.pick' import pick from 'lodash.pick'
import Store from 'electron-store' import Store from 'electron-store'
import StateMachine from 'javascript-state-machine' import StateMachine from 'javascript-state-machine'
import { mainLog } from '../utils/log'
import { isLndRunning } from '../lnd/util'
import LndConfig from '../lnd/config' import LndConfig from '../lnd/config'
import Lightning from '../lnd/lightning' import Lightning from '../lnd/lightning'
import Neutrino from '../lnd/neutrino' import Neutrino from '../lnd/neutrino'
import { initWalletUnlocker } from '../lnd/walletUnlocker' import WalletUnlocker from '../lnd/walletUnlocker'
import { mainLog } from '../utils/log'
import { isLndRunning } from '../lnd/util'
type onboardingOptions = { type onboardingOptions = {
type: 'local' | 'custom' | 'btcpayserver', type: 'local' | 'custom' | 'btcpayserver',
@ -53,6 +54,7 @@ class ZapController {
mainWindow: BrowserWindow mainWindow: BrowserWindow
neutrino: Neutrino neutrino: Neutrino
lightning: Lightning lightning: Lightning
walletUnlocker: WalletUnlocker
splashScreenTime: number splashScreenTime: number
lndConfig: LndConfig lndConfig: LndConfig
_fsm: StateMachine _fsm: StateMachine
@ -191,6 +193,16 @@ class ZapController {
else if (e.code === 'LND_GRPC_MACAROON_ERROR') { else if (e.code === 'LND_GRPC_MACAROON_ERROR') {
errors.macaroon = e.message errors.macaroon = e.message
} }
// The `startLightningWallet` call attempts to call the `getInfo` method on the Lightning service in order to
// verify that it is accessible. If it is not, an error 12 is throw whcih is the gRPC code for `UNIMPLEMENTED`
// which indicates that the requested operation is not implemented or not supported/enabled in the service.
// See https://github.com/grpc/grpc-node/blob/master/packages/grpc-native-core/src/constants.js#L129
if (e.code === 12) {
errors.host =
'Unable to connect to host. Please ensure wallet is unlocked before connecting.'
}
// Other error codes such as UNAVAILABLE most likely indicate that there is a problem with the host. // Other error codes such as UNAVAILABLE most likely indicate that there is a problem with the host.
else { else {
errors.host = `Unable to connect to host: ${e.details || e.message}` errors.host = `Unable to connect to host: ${e.details || e.message}`
@ -244,24 +256,24 @@ class ZapController {
/** /**
* Start the wallet unlocker. * Start the wallet unlocker.
*/ */
startWalletUnlocker() { async startWalletUnlocker() {
mainLog.info('Establishing connection to Wallet Unlocker gRPC interface...') mainLog.info('Establishing connection to Wallet Unlocker gRPC interface...')
this.walletUnlocker = new WalletUnlocker(this.lndConfig)
// Connect to the WalletUnlocker interface.
try { try {
const walletUnlockerMethods = initWalletUnlocker(this.lndConfig) await this.walletUnlocker.connect()
// Listen for all gRPC restful methods // Listen for all gRPC restful methods and pass to gRPC.
ipcMain.on('walletUnlocker', (event, { msg, data }) => { ipcMain.on('walletUnlocker', (event, { msg, data }) =>
walletUnlockerMethods(event, msg, data) this.walletUnlocker.registerMethods(event, msg, data)
}) )
// Notify the renderer that the wallet unlocker is active. // Notify the renderer that the wallet unlocker is active.
this.sendMessage('walletUnlockerGrpcActive') this.sendMessage('walletUnlockerGrpcActive')
} catch (error) { } catch (err) {
dialog.showMessageBox({ mainLog.warn('Unable to connect to WalletUnlocker gRPC interface: %o', err)
type: 'error', throw err
message: `Unable to start lnd wallet unlocker. Please check your lnd node and try again: ${error}`
})
app.quit()
} }
} }
@ -279,7 +291,7 @@ class ZapController {
this.lightning.subscribe(this.mainWindow) this.lightning.subscribe(this.mainWindow)
// Listen for all gRPC restful methods and pass to gRPC. // Listen for all gRPC restful methods and pass to gRPC.
ipcMain.on('lnd', (event, { msg, data }) => this.lightning.lndMethods(event, msg, data)) ipcMain.on('lnd', (event, { msg, data }) => this.lightning.registerMethods(event, msg, data))
// Let the renderer know that we are connected. // Let the renderer know that we are connected.
this.sendMessage('lightningGrpcActive') this.sendMessage('lightningGrpcActive')

2
app/reducers/info.js

@ -72,8 +72,6 @@ const networks = {
unitPrefix: '' unitPrefix: ''
} }
} }
// IPC info fetch failed
// export const infoFailed = (event, data) => dispatch => {}
// ------------------------------------ // ------------------------------------
// Action Handlers // Action Handlers

2
test/unit/lnd/lightning.spec.js

@ -15,7 +15,7 @@ describe('Lightning', function() {
expect(this.lightning.mainWindow).toBeNull() expect(this.lightning.mainWindow).toBeNull()
}) })
it('should set the "lnd" property to null', () => { it('should set the "lnd" property to null', () => {
expect(this.lightning.lnd).toBeNull() expect(this.lightning.service).toBeNull()
}) })
it('should initialise the "subscriptions" object with null values', () => { it('should initialise the "subscriptions" object with null values', () => {
expect(this.lightning.subscriptions).toMatchObject({ expect(this.lightning.subscriptions).toMatchObject({

Loading…
Cancel
Save