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 6 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 subscribeToInvoices from './subscribe/invoices'
import subscribeToChannelGraph from './subscribe/channelgraph'
import { getInfo } from './methods/networkController'
// Type definition for subscriptions property.
type LightningSubscriptionsType = {
@ -25,7 +26,7 @@ type LightningSubscriptionsType = {
*/
class Lightning {
mainWindow: BrowserWindow
lnd: any
service: any
lndConfig: LndConfig
subscriptions: LightningSubscriptionsType
_fsm: StateMachine
@ -40,7 +41,7 @@ class Lightning {
constructor(lndConfig: LndConfig) {
this.mainWindow = null
this.lnd = null
this.service = null
this.lndConfig = lndConfig
this.subscriptions = {
channelGraph: null,
@ -65,42 +66,48 @@ class Lightning {
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)
// 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.lnd = new rpc.lnrpc.Lightning(host, credentials)
// Wait for the gRPC connection to be established.
return new Promise((resolve, reject) => {
grpc.waitForClientReady(this.lnd, getDeadline(2), err => {
if (err) {
return reject(err)
}
return resolve()
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()
})
})
})
})
.then(() => getInfo(this.service))
.catch(err => {
this.service.close()
throw err
})
}
/**
@ -109,8 +116,8 @@ class Lightning {
onBeforeDisconnect() {
mainLog.info('Disconnecting from Lightning gRPC service')
this.unsubscribe()
if (this.lnd) {
this.lnd.close()
if (this.service) {
this.service.close()
}
}
@ -121,7 +128,7 @@ class Lightning {
mainLog.info('Shutting down Lightning daemon')
this.unsubscribe()
return new Promise((resolve, reject) => {
this.lnd.stopDaemon({}, (err, data) => {
this.service.stopDaemon({}, (err, data) => {
if (err) {
return reject(err)
}
@ -137,8 +144,8 @@ class Lightning {
/**
* Hook up lnd restful methods.
*/
lndMethods(event: Event, msg: string, data: any) {
return methods(this.lnd, mainLog, event, msg, data)
registerMethods(event: Event, msg: string, data: any) {
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])
return infoData
})
.catch(() => event.sender.send('infoFailed'))
.catch(error => log.error('info:', error))
break
case 'describeNetwork':
networkController

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

@ -2,7 +2,7 @@ import { status } from 'grpc'
import { mainLog } from '../../utils/log'
export default function subscribeToChannelGraph() {
const call = this.lnd.subscribeChannelGraph({})
const call = this.service.subscribeChannelGraph({})
call.on('data', 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'
export default function subscribeToInvoices() {
const call = this.lnd.subscribeInvoices({})
const call = this.service.subscribeInvoices({})
call.on('data', 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'
export default function subscribeToTransactions() {
const call = this.lnd.subscribeTransactions({})
const call = this.service.subscribeTransactions({})
call.on('data', 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 { 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'
export const initWalletUnlocker = lndConfig => {
const walletUnlockerObj = walletUnlocker(lndConfig)
const walletUnlockerMethodsCallback = (event, msg, data) =>
walletUnlockerMethods(lndConfig, walletUnlockerObj, mainLog, event, msg, data)
/**
* Creates an LND grpc client lightning service.
* @returns {WalletUnlocker}
*/
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 => {
const lndCert = fs.readFileSync(lndConfig.cert)
const credentials = grpc.credentials.createSsl(lndCert)
// 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
// 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.WalletUnlocker(host, credentials)
// 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 * 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 => {
switch (error.code) {
// 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 Store from 'electron-store'
import StateMachine from 'javascript-state-machine'
import { mainLog } from '../utils/log'
import { isLndRunning } from '../lnd/util'
import LndConfig from '../lnd/config'
import Lightning from '../lnd/lightning'
import Neutrino from '../lnd/neutrino'
import { initWalletUnlocker } from '../lnd/walletUnlocker'
import { mainLog } from '../utils/log'
import { isLndRunning } from '../lnd/util'
import WalletUnlocker from '../lnd/walletUnlocker'
type onboardingOptions = {
type: 'local' | 'custom' | 'btcpayserver',
@ -53,6 +54,7 @@ class ZapController {
mainWindow: BrowserWindow
neutrino: Neutrino
lightning: Lightning
walletUnlocker: WalletUnlocker
splashScreenTime: number
lndConfig: LndConfig
_fsm: StateMachine
@ -191,6 +193,16 @@ class ZapController {
else if (e.code === 'LND_GRPC_MACAROON_ERROR') {
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.
else {
errors.host = `Unable to connect to host: ${e.details || e.message}`
@ -244,24 +256,24 @@ class ZapController {
/**
* Start the wallet unlocker.
*/
startWalletUnlocker() {
async startWalletUnlocker() {
mainLog.info('Establishing connection to Wallet Unlocker gRPC interface...')
this.walletUnlocker = new WalletUnlocker(this.lndConfig)
// Connect to the WalletUnlocker interface.
try {
const walletUnlockerMethods = initWalletUnlocker(this.lndConfig)
await this.walletUnlocker.connect()
// Listen for all gRPC restful methods
ipcMain.on('walletUnlocker', (event, { msg, data }) => {
walletUnlockerMethods(event, msg, data)
})
// Listen for all gRPC restful methods and pass to gRPC.
ipcMain.on('walletUnlocker', (event, { msg, data }) =>
this.walletUnlocker.registerMethods(event, msg, data)
)
// Notify the renderer that the wallet unlocker is active.
this.sendMessage('walletUnlockerGrpcActive')
} catch (error) {
dialog.showMessageBox({
type: 'error',
message: `Unable to start lnd wallet unlocker. Please check your lnd node and try again: ${error}`
})
app.quit()
} catch (err) {
mainLog.warn('Unable to connect to WalletUnlocker gRPC interface: %o', err)
throw err
}
}
@ -279,7 +291,7 @@ class ZapController {
this.lightning.subscribe(this.mainWindow)
// 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.
this.sendMessage('lightningGrpcActive')

2
app/reducers/info.js

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

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

@ -15,7 +15,7 @@ describe('Lightning', function() {
expect(this.lightning.mainWindow).toBeNull()
})
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', () => {
expect(this.lightning.subscriptions).toMatchObject({

Loading…
Cancel
Save