Browse Source

feat(lnd): support multiple lnd configurations

Store lnd data within the Zap datadir and prepare to be able to support
multiple lnd configurations.
renovate/lint-staged-8.x
Tom Kirkpatrick 7 years ago
parent
commit
56c6c59180
No known key found for this signature in database GPG Key ID: 72203A8EC5967EA8
  1. 1
      app/containers/Root.js
  2. 346
      app/lib/lnd/config.js
  3. 18
      app/lib/lnd/index.js
  4. 6
      app/lib/lnd/lightning.js
  5. 26
      app/lib/lnd/neutrino.js
  6. 45
      app/lib/lnd/util.js
  7. 18
      app/lib/lnd/walletUnlocker.js
  8. 5
      app/lib/lnd/walletUnlockerMethods/index.js
  9. 2
      app/lib/utils/log.js
  10. 111
      app/lib/zap/controller.js
  11. 41
      app/reducers/onboarding.js
  12. 3
      package.json
  13. 1
      test/unit/__mocks__/grpc.js
  14. 208
      test/unit/lnd/lnd-config.spec.js
  15. 8
      yarn.lock

1
app/containers/Root.js

@ -1,4 +1,3 @@
// @flow
import React from 'react'
import { Provider, connect } from 'react-redux'
import { ConnectedRouter } from 'react-router-redux'

346
app/lib/lnd/config.js

@ -1,92 +1,292 @@
import { homedir, platform } from 'os'
import { dirname, join, normalize } from 'path'
import Store from 'electron-store'
// @flow
import { join } from 'path'
import { app } from 'electron'
import isDev from 'electron-is-dev'
import Store from 'electron-store'
import pick from 'lodash.pick'
import createDebug from 'debug'
import untildify from 'untildify'
import tildify from 'tildify'
import { appRootPath, binaryPath } from './util'
const debug = createDebug('zap:lnd-config')
// Supported connection types.
const types = {
local: 'Local',
custom: 'Custom',
btcpayserver: 'BTCPay Server'
}
// Get a path to prepend to any nodejs calls that are getting at files in the package,
// so that it works both from source and in an asar-packaged mac app.
// See https://github.com/electron-userland/electron-builder/issues/751
//
// windows from source: "C:\myapp\node_modules\electron\dist\resources\default_app.asar"
// mac from source: "/Users/me/dev/myapp/node_modules/electron/dist/Electron.app/Contents/Resources/default_app.asar"
// mac from a package: <somewhere>"/my.app/Contents/Resources/app.asar"
//
// If we are run from outside of a packaged app, our working directory is the right place to be.
// And no, we can't just set our working directory to somewhere inside the asar. The OS can't handle that.
const appPath = app.getAppPath()
const appRootPath = appPath.indexOf('default_app.asar') < 0 ? normalize(`${appPath}/..`) : ''
// Get the name of the current platform which we can use to determine the tlsCertPathation of various lnd resources.
const plat = platform()
// Get the OS specific default lnd data dir and binary name.
let lndDataDir
let lndBin
switch (plat) {
case 'darwin':
lndDataDir = join(homedir(), 'Library', 'Application Support', 'Lnd')
lndBin = 'lnd'
break
case 'linux':
lndDataDir = join(homedir(), '.lnd')
lndBin = 'lnd'
break
case 'win32':
lndDataDir = join(process.env.LOCALAPPDATA, 'Local', 'Lnd')
lndBin = 'lnd.exe'
break
default:
break
// Supported currencies.
const currencties = {
bitcoin: 'Bitcoin',
litecoin: 'Litecoin'
}
// Get the path to the lnd binary.
let lndPath
if (isDev) {
lndPath = join(dirname(require.resolve('lnd-binary/package.json')), 'vendor', lndBin)
} else {
lndPath = join(appRootPath, 'bin', lndBin)
// Supported networks.
const networks = {
mainnet: 'Mainnet',
testnet: 'Testnet'
}
// Type definition for for local connection settings.
type LndConfigSettingsLocalType = {|
alias?: string,
autopilot?: boolean
|}
// Type definition for for custom connection settings.
type LndConfigSettingsCustomType = {|
host: string,
cert: string,
macaroon: string
|}
// Type definition for for BTCPay Server connection settings.
type LndConfigSettingsBtcPayServerType = {|
string: string,
host: string,
macaroon: string
|}
// Type definition for for BTCPay Server connection settings.
type LndConfigSettingsType = {|
type: $Keys<typeof types>,
currency: $Keys<typeof currencties>,
network: $Keys<typeof networks>,
wallet: string
|}
// Type definition for LndConfig constructor options.
type LndConfigOptions = {|
...LndConfigSettingsType,
settings?:
| LndConfigSettingsLocalType
| LndConfigSettingsCustomType
| LndConfigSettingsBtcPayServerType
|}
const _host = new WeakMap()
const _cert = new WeakMap()
const _macaroon = new WeakMap()
const _string = new WeakMap()
/**
* Utility methods to clean and prepare data.
*/
const safeTrim = <T>(val: ?T): ?T => (typeof val === 'string' ? val.trim() : val)
const safeTildify = <T>(val: ?T): ?T => (typeof val === 'string' ? tildify(val) : val)
const safeUntildify = <T>(val: ?T): ?T => (typeof val === 'string' ? untildify(val) : val)
/**
* Get current lnd configuration.
*
* Cert and Macaroon will be at one of the following destinations depending on your machine:
* Mac OS X: ~/Library/Application Support/Lnd/tls.cert
* Linux: ~/.lnd/tls.cert
* Windows: C:\Users\...\AppData\Local\Lnd
*
* @return {object} current lnd configuration options.
* LndConfig class
*/
const lnd = () => {
// Get an electron store named 'connection' in which the saved connection detailes are stored.
const store = new Store({ name: 'connection' })
class LndConfig {
static DEFAULT_CONFIG = {
type: 'local',
currency: 'bitcoin',
network: 'testnet',
wallet: 'wallet-1'
}
static SETTINGS_PROPS = {
local: ['alias', 'autopilot'],
custom: ['host', 'cert', 'macaroon'],
btcpayserver: ['host', 'macaroon', 'string']
}
static store = new Store({ name: 'connection' })
// Type descriptor properties.
type: string
currency: string
network: string
wallet: string
// User configurable settings.
host: ?string
cert: ?string
macaroon: ?string
string: ?string
alias: ?string
autopilot: ?boolean
// Read only data properties.
+key: string
+binaryPath: string
+dataDir: string
+configPath: string
+rpcProtoPath: string
/**
* Fetch a config option from the connection store.
* if undefined fallback to a path relative to the lnd data dir.
* Lnd configuration class.
*
* @param {string} name name of property to fetch from the store.
* @param {string} path path relative to the lnd data dir.
* @return {string} config param or filepath relative to the lnd data dir.
* @param {LndConfigOptions} [options] Lnd config options.
* @param {string} options.type config type (Local|custom|btcpayserver)
* @param {string} options.currency config currency (bitcoin|litecoin)
* @param {string} options.network config network (mainnet|testnet)
* @param {string} options.wallet config wallet name (eg wallet-1)
* @param {Object} [options.settings] config settings used to initialise the config with.
*/
const getFromStoreOrDataDir = (name, file) => {
let path = store.get(name)
if (typeof path === 'undefined') {
path = join(lndDataDir, file)
constructor(options?: LndConfigOptions) {
debug('Constructor called with options: %o', options)
// Define properties that we support with custom getters and setters as needed.
// flow currently doesn't support defineProperties properly (https://github.com/facebook/flow/issues/285)
const { defineProperties } = Object
defineProperties(this, {
key: {
get() {
return `${this.type}.${this.currency}.${this.network}.${this.wallet}`
}
},
binaryPath: {
enumerable: true,
value: binaryPath
},
dataDir: {
enumerable: true,
get() {
return join(app.getPath('userData'), 'lnd', this.currency, this.network, this.wallet)
}
},
configPath: {
enumerable: true,
get() {
return join(appRootPath, 'resources', 'lnd.conf')
}
},
rpcProtoPath: {
enumerable: true,
get() {
return join(appRootPath, 'resources', 'rpc.proto')
}
},
// Getters / Setters for host property.
// - Trim value before saving.
host: {
enumerable: true,
get() {
return _host.get(this)
},
set(value: string) {
_host.set(this, safeTrim(value))
}
},
// Getters / Setters for cert property.
// - Untildify value on retrieval.
// - Trim value before saving.
cert: {
enumerable: true,
get() {
return safeUntildify(_cert.get(this))
},
set(value: string) {
_cert.set(this, safeTrim(value))
}
},
// Getters / Setters for macaroon property.
// - Untildify value on retrieval.
// - Trim value before saving.
macaroon: {
enumerable: true,
get() {
return safeUntildify(_macaroon.get(this))
},
set(value: string) {
_macaroon.set(this, safeTrim(value))
}
},
// Getters / Setters for string property.
// - Trim value before saving.
string: {
enumerable: true,
get() {
return _string.get(this)
},
set(value: string) {
_string.set(this, safeTrim(value))
}
}
})
// If options were provided, use them to initialise the instance.
if (options) {
this.type = options.type
this.currency = options.currency
this.network = options.network
this.wallet = options.wallet
// If settings were provided then clean them up and assign them to the instance for easy access.
if (options.settings) {
debug('Setting settings as: %o', options.settings)
Object.assign(this, options.settings)
}
}
// If no options were provided load the details of the current active or default wallet.
else {
const settings = new Store({ name: 'settings' })
const activeConnection: ?LndConfigSettingsType = settings.get('activeConnection')
debug('Determined active connection as: %o', activeConnection)
if (activeConnection && Object.keys(activeConnection).length > 0) {
debug('Assigning connection details from activeConnection as: %o', activeConnection)
Object.assign(this, activeConnection)
}
// If the connection settings were not found for the configured active connection, load the default values.
debug('Fetching connection config for %s', this.key)
if (!this.key || (this.key && !LndConfig.store.has(this.key))) {
debug('Active connection config not found. Setting config as: %o', LndConfig.DEFAULT_CONFIG)
Object.assign(this, LndConfig.DEFAULT_CONFIG)
}
}
// For local configs host/cert/macaroon are auto-generated.
if (this.type === 'local') {
const defaultLocalOptions = {
host: 'localhost:10009',
cert: join(this.dataDir, 'tls.cert'),
macaroon: join(this.dataDir, 'admin.macaroon')
}
debug('Connection type is local. Assigning settings as: %o', defaultLocalOptions)
Object.assign(this, defaultLocalOptions)
}
return untildify(path)
}
return {
lndPath,
configPath: join(appRootPath, 'resources', 'lnd.conf'),
rpcProtoPath: join(appRootPath, 'resources', 'rpc.proto'),
host: store.get('host', 'localhost:10009'),
cert: getFromStoreOrDataDir('cert', 'tls.cert'),
macaroon: getFromStoreOrDataDir('macaroon', 'admin.macaroon')
/**
* Load settings for this configuration from the store.
* @return {LndConfig} Updated LndConfig object.
*/
load() {
const settings = pick(LndConfig.store.get(this.key, {}), LndConfig.SETTINGS_PROPS[this.type])
debug('Loaded settings for %s config as: %o', this.key, settings)
return Object.assign(this, settings)
}
/**
* Save settings for this configuration to the store.
* @return {LndConfig} Updated LndConfig object.
*/
save() {
const settings = pick(this, LndConfig.SETTINGS_PROPS[this.type])
// Tildify cert and macaroon values before storing for better portability.
if (settings.cert) {
settings.cert = safeTildify(settings.cert)
}
if (settings.macaroon) {
settings.macaroon = safeTildify(settings.macaroon)
}
debug('Saving settings for %s config as: %o', this.key, settings)
LndConfig.store.set(this.key, settings)
return this
}
}
export default { lnd }
export default LndConfig

18
app/lib/lnd/index.js

@ -1,18 +0,0 @@
import config from './config'
import walletUnlocker from './walletUnlocker'
import walletUnlockerMethods from './walletUnlockerMethods'
// use mainLog because lndLog is reserved for the lnd binary itself
import { mainLog } from '../utils/log'
const initWalletUnlocker = () => {
const lndConfig = config.lnd()
const walletUnlockerObj = walletUnlocker(lndConfig.rpcProtoPath, lndConfig.host)
const walletUnlockerMethodsCallback = (event, msg, data) =>
walletUnlockerMethods(walletUnlockerObj, mainLog, event, msg, data)
return walletUnlockerMethodsCallback
}
export default {
initWalletUnlocker
}

6
app/lib/lnd/lightning.js

@ -1,6 +1,5 @@
import grpc from 'grpc'
import { loadSync } from '@grpc/proto-loader'
import config from './config'
import { getDeadline, validateHost, createSslCreds, createMacaroonCreds } from './util'
import methods from './methods'
import { mainLog } from '../utils/log'
@ -27,9 +26,8 @@ class Lightning {
* Connect to the gRPC interface and verify it is functional.
* @return {Promise<rpc.lnrpc.Lightning>}
*/
async connect() {
const lndConfig = config.lnd()
const { host, rpcProtoPath, cert, macaroon } = lndConfig
async connect(lndConfig) {
const { rpcProtoPath, host, cert, macaroon } = lndConfig
// Verify that the host is valid before creating a gRPC client that is connected to it.
return await validateHost(host).then(async () => {

26
app/lib/lnd/neutrino.js

@ -1,7 +1,6 @@
import split2 from 'split2'
import { spawn } from 'child_process'
import EventEmitter from 'events'
import config from './config'
import { mainLog, lndLog, lndLogGetLevel } from '../utils/log'
import { fetchBlockHeight } from './util'
@ -24,10 +23,9 @@ const GOT_LND_BLOCK_HEIGHT = 'got-lnd-block-height'
* @extends EventEmitter
*/
class Neutrino extends EventEmitter {
constructor(alias, autopilot) {
constructor(lndConfig) {
super()
this.alias = alias
this.autopilot = autopilot
this.lndConfig = lndConfig
this.process = null
this.walletUnlockerGrpcActive = false
this.lightningGrpcActive = false
@ -43,21 +41,21 @@ class Neutrino extends EventEmitter {
throw new Error('Neutrino process with PID ${this.process.pid} already exists.')
}
const lndConfig = config.lnd()
mainLog.info('Starting lnd in neutrino mode')
mainLog.debug(' > lndPath', lndConfig.lndPath)
mainLog.debug(' > rpcProtoPath:', lndConfig.rpcProtoPath)
mainLog.debug(' > host:', lndConfig.host)
mainLog.debug(' > cert:', lndConfig.cert)
mainLog.debug(' > macaroon:', lndConfig.macaroon)
mainLog.info(' > binaryPath', this.lndConfig.binaryPath)
mainLog.info(' > rpcProtoPath:', this.lndConfig.rpcProtoPath)
mainLog.info(' > host:', this.lndConfig.host)
mainLog.info(' > cert:', this.lndConfig.cert)
mainLog.info(' > macaroon:', this.lndConfig.macaroon)
const neutrinoArgs = [
`--configfile=${lndConfig.configPath}`,
`${this.autopilot ? '--autopilot.active' : ''}`,
`${this.alias ? `--alias=${this.alias}` : ''}`
`--configfile=${this.lndConfig.configPath}`,
`--lnddir=${this.lndConfig.dataDir}`,
`${this.lndConfig.autopilot ? '--autopilot.active' : ''}`,
`${this.lndConfig.alias ? `--alias=${this.lndConfig.alias}` : ''}`
]
this.process = spawn(lndConfig.lndPath, neutrinoArgs)
this.process = spawn(this.lndConfig.binaryPath, neutrinoArgs)
.on('error', error => this.emit(ERROR, error))
.on('close', code => {
this.emit(CLOSE, code)

45
app/lib/lnd/util.js

@ -2,8 +2,11 @@ import dns from 'dns'
import fs from 'fs'
import axios from 'axios'
import { promisify } from 'util'
import { basename, dirname, join, normalize } from 'path'
import { platform } from 'os'
import { lookup } from 'ps-node'
import path from 'path'
import { app } from 'electron'
import isDev from 'electron-is-dev'
import grpc from 'grpc'
import isIP from 'validator/lib/isIP'
import isPort from 'validator/lib/isPort'
@ -13,6 +16,44 @@ import { mainLog } from '../utils/log'
const fsReadFile = promisify(fs.readFile)
const dnsLookup = promisify(dns.lookup)
// ------------------------------------
// Constants
// ------------------------------------
/**
* Get a path to prepend to any nodejs calls that are getting at files in the package,
* so that it works both from source and in an asar-packaged mac app.
* See https://github.com/electron-userland/electron-builder/issues/751
*
* windows from source: "C:\myapp\node_modules\electron\dist\resources\default_app.asar"
* mac from source: "/Users/me/dev/myapp/node_modules/electron/dist/Electron.app/Contents/Resources/default_app.asar"
* mac from a package: <appRootPathsomewhere>"/my.app/Contents/Resources/app.asar"
*
* If we are run from outside of a packaged app, our working directory is the right place to be.
* And no, we can't just set our working directory to somewhere inside the asar. The OS can't handle that.
* @return {String} Path to the lnd binary.
*/
export const appRootPath =
app.getAppPath().indexOf('default_app.asar') < 0 ? normalize(`${app.getAppPath()}/..`) : ''
/**
* Get the OS specific lnd binary name.
* @return {String} 'lnd' on mac or linux, 'lnd.exe' on windows.
*/
export const binaryName = platform() === 'win32' ? 'lnd.exe' : 'lnd'
/**
* Get the OS specific path to the lnd binary.
* @return {String} Path to the lnd binary.
*/
export const binaryPath = isDev
? join(dirname(require.resolve('lnd-binary/package.json')), 'vendor', binaryName)
: join(appRootPath, 'bin', binaryName)
// ------------------------------------
// Helpers
// ------------------------------------
/**
* Helper function to get the current block height.
* @return {Number} The current block height.
@ -123,7 +164,7 @@ 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 === path.basename(macaroonPath)) {
if (macaroonPath === basename(macaroonPath)) {
metadata.add('macaroon', macaroonPath)
} else {
const macaroon = await fsReadFile(macaroonPath).catch(e => {

18
app/lib/lnd/walletUnlocker.js

@ -1,10 +1,18 @@
import fs from 'fs'
import grpc from 'grpc'
import { loadSync } from '@grpc/proto-loader'
import config from './config'
import walletUnlockerMethods from './walletUnlockerMethods'
import { mainLog } from '../utils/log'
const walletUnlocker = (rpcpath, host) => {
const lndConfig = config.lnd()
export const initWalletUnlocker = lndConfig => {
const walletUnlockerObj = walletUnlocker(lndConfig)
const walletUnlockerMethodsCallback = (event, msg, data) =>
walletUnlockerMethods(lndConfig, walletUnlockerObj, mainLog, event, msg, data)
return walletUnlockerMethodsCallback
}
export const walletUnlocker = lndConfig => {
const lndCert = fs.readFileSync(lndConfig.cert)
const credentials = grpc.credentials.createSsl(lndCert)
@ -24,7 +32,5 @@ const walletUnlocker = (rpcpath, host) => {
const rpc = grpc.loadPackageDefinition(packageDefinition)
// Instantiate a new connection to the WalletUnlocker interface.
return new rpc.lnrpc.WalletUnlocker(host, credentials)
return new rpc.lnrpc.WalletUnlocker(lndConfig.host, credentials)
}
export default walletUnlocker

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

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

2
app/lib/utils/log.js

@ -8,7 +8,7 @@ debugLogger.inspectOptions = {
// Enable all zap logs if DEBUG has not been explicitly set.
if (!process.env.DEBUG) {
// debugLogger.debug.enable('zap:*')
process.env.DEBUG = 'zap:*'
process.env.DEBUG = 'zap:main,zap:lnd,zap:updater'
}
if (!process.env.DEBUG_LEVEL) {
process.env.DEBUG_LEVEL = 'info'

111
app/lib/zap/controller.js

@ -1,12 +1,25 @@
import { app, ipcMain, dialog } from 'electron'
// @flow
import { app, ipcMain, dialog, BrowserWindow } from 'electron'
import pick from 'lodash.pick'
import Store from 'electron-store'
import StateMachine from 'javascript-state-machine'
import lnd from '../lnd'
import LndConfig from '../lnd/config'
import Neutrino from '../lnd/neutrino'
import Lightning from '../lnd/lightning'
import { initWalletUnlocker } from '../lnd/walletUnlocker'
import { mainLog } from '../utils/log'
import { isLndRunning } from '../lnd/util'
type onboardingOptions = {
type: 'local' | 'custom' | 'btcpayserver',
host?: string,
cert?: string,
macaroon?: string,
alias?: string,
autopilot?: boolean
}
const grpcSslCipherSuites = connectionType =>
(connectionType === 'btcpayserver'
? [
@ -37,11 +50,26 @@ const grpcSslCipherSuites = connectionType =>
* The ZapController class coordinates actions between the the main nand renderer processes.
*/
class ZapController {
mainWindow: BrowserWindow
neutrino: Neutrino
lightning: Lightning
splashScreenTime: number
lightningGrpcConnected: boolean
lndConfig: LndConfig
_fsm: StateMachine
// Transitions provided by the state machine.
startOnboarding: any
startLnd: any
connectLnd: any
terminate: any
is: any
/**
* Create a new ZapController instance.
* @param {BrowserWindow} mainWindow BrowserWindow instance to interact with.
*/
constructor(mainWindow) {
constructor(mainWindow: BrowserWindow) {
// Variable to hold the main window instance.
this.mainWindow = mainWindow
@ -59,6 +87,10 @@ class ZapController {
// Initialize the state machine.
this._fsm()
// Initialise the controler with the current active config.
this.lndConfig = new LndConfig()
this.lndConfig.load()
}
/**
@ -101,11 +133,12 @@ class ZapController {
onStartOnboarding() {
mainLog.debug('[FSM] onStartOnboarding...')
this.sendMessage('startOnboarding')
this.sendMessage('startOnboarding', this.lndConfig)
}
onStartLnd(options) {
onStartLnd() {
mainLog.debug('[FSM] onStartLnd...')
return isLndRunning().then(res => {
if (res) {
mainLog.error('lnd already running: %s', res)
@ -115,20 +148,23 @@ class ZapController {
})
return app.quit()
}
mainLog.info('Starting new lnd instance')
mainLog.info(' > alias:', options.alias)
mainLog.info(' > autopilot:', options.autopilot)
return this.startNeutrino(options.alias, options.autopilot)
mainLog.info(' > alias:', this.lndConfig.alias)
mainLog.info(' > autopilot:', this.lndConfig.autopilot)
return this.startNeutrino()
})
}
onConnectLnd(options) {
onConnectLnd() {
mainLog.debug('[FSM] onConnectLnd...')
mainLog.info('Connecting to custom lnd instance')
mainLog.info(' > host:', options.host)
mainLog.info(' > cert:', options.cert)
mainLog.info(' > macaroon:', options.macaroon)
this.startLightningWallet()
mainLog.info(' > host:', this.lndConfig.host)
mainLog.info(' > cert:', this.lndConfig.cert)
mainLog.info(' > macaroon:', this.lndConfig.macaroon)
return this.startLightningWallet()
.then(() => this.sendMessage('finishOnboarding'))
.catch(e => {
const errors = {}
@ -183,7 +219,7 @@ class ZapController {
* @param {string} msg message to send.
* @param {[type]} data additional data to acompany the message.
*/
sendMessage(msg, data) {
sendMessage(msg: string, data: any) {
if (this.mainWindow) {
mainLog.info('Sending message to renderer process: %o', { msg, data })
this.mainWindow.webContents.send(msg, data)
@ -201,7 +237,7 @@ class ZapController {
startWalletUnlocker() {
mainLog.info('Starting wallet unlocker...')
try {
const walletUnlockerMethods = lnd.initWalletUnlocker()
const walletUnlockerMethods = initWalletUnlocker(this.lndConfig)
// Listen for all gRPC restful methods
ipcMain.on('walletUnlocker', (event, { msg, data }) => {
@ -227,7 +263,7 @@ class ZapController {
this.lightning = new Lightning()
// Connect to the Lightning interface.
await this.lightning.connect()
await this.lightning.connect(this.lndConfig)
// Subscribe the main window to receive streams.
this.lightning.subscribe(this.mainWindow)
@ -265,9 +301,9 @@ class ZapController {
* @param {boolean} autopilot True if autopilot should be enabled.
* @return {Neutrino} Neutrino instance.
*/
startNeutrino(alias, autopilot) {
startNeutrino() {
mainLog.info('Starting Neutrino...')
this.neutrino = new Neutrino(alias, autopilot)
this.neutrino = new Neutrino(this.lndConfig)
this.neutrino.on('error', error => {
mainLog.error(`Got error from lnd process: ${error})`)
@ -279,7 +315,7 @@ class ZapController {
this.neutrino.on('close', code => {
mainLog.info(`Lnd process has shut down (code ${code})`)
if (['running', 'connected'].includes(this.state)) {
if (this.is('running') || this.is('connected')) {
dialog.showMessageBox({
type: 'error',
message: `Lnd has unexpectadly quit`
@ -324,18 +360,27 @@ class ZapController {
this.neutrino.start()
}
finishOnboarding(options) {
// Trim any user supplied strings.
const cleanOptions = Object.keys(options).reduce((previous, current) => {
previous[current] =
typeof options[current] === 'string' ? options[current].trim() : options[current]
return previous
}, {})
// Save the options.
const store = new Store({ name: 'connection' })
store.store = cleanOptions
mainLog.info('Saved lnd config to %s: %o', store.path, store.store)
finishOnboarding(options: onboardingOptions) {
mainLog.info('Finishing onboarding')
// Save the lnd config options that we got from the renderer.
this.lndConfig = new LndConfig({
type: options.type,
currency: 'bitcoin',
network: 'testnet',
wallet: 'wallet-1',
settings: pick(options, LndConfig.SETTINGS_PROPS[options.type])
})
this.lndConfig.save()
// Set as the active config.
const settings = new Store({ name: 'settings' })
settings.set('activeConnection', {
type: this.lndConfig.type,
currency: this.lndConfig.currency,
network: this.lndConfig.network,
wallet: this.lndConfig.wallet
})
mainLog.info('Saved active connection as: %o', settings.get('activeConnection'))
// Set up SSL with the cypher suits that we need based on the connection type.
process.env.GRPC_SSL_CIPHER_SUITES =
@ -343,14 +388,14 @@ class ZapController {
// If the requested connection type is a local one then start up a new lnd instance.
// // Otherwise attempt to connect to an lnd instance using user supplied connection details.
cleanOptions.type === 'local' ? this.startLnd() : this.connectLnd()
return options.type === 'local' ? this.startLnd() : this.connectLnd()
}
/**
* Add IPC event listeners...
*/
_registerIpcListeners() {
ipcMain.on('startLnd', (event, options = {}) => this.finishOnboarding(options))
ipcMain.on('startLnd', (event, options: onboardingOptions) => this.finishOnboarding(options))
}
}

41
app/reducers/onboarding.js

@ -1,10 +1,7 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import Store from 'electron-store'
import get from 'lodash.get'
const store = new Store({ name: 'connection' })
// ------------------------------------
// Constants
// ------------------------------------
@ -230,7 +227,25 @@ export const submitNewWallet = (
dispatch({ type: CREATING_NEW_WALLET })
}
export const startOnboarding = () => dispatch => {
// Listener for errors connecting to LND gRPC
export const startOnboarding = (event, lndConfig = {}) => dispatch => {
dispatch(setConnectionType(lndConfig.type))
switch (lndConfig.type) {
case 'local':
dispatch(updateAlias(lndConfig.alias))
dispatch(setAutopilot(lndConfig.autopilot))
break
case 'custom':
dispatch(setConnectionHost(lndConfig.host))
dispatch(setConnectionCert(lndConfig.cert))
dispatch(setConnectionMacaroon(lndConfig.macaroon))
break
case 'btcpayserver':
dispatch(setConnectionString(lndConfig.string))
break
}
dispatch({ type: ONBOARDING_STARTED })
}
@ -447,16 +462,18 @@ export { onboardingSelectors }
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
onboarding: false,
onboarded: false,
step: 0.1,
connectionType: store.get('type', 'local'),
connectionString: store.get('string', ''),
connectionHost: store.get('host', ''),
connectionCert: store.get('cert', ''),
connectionMacaroon: store.get('macaroon', ''),
alias: store.get('alias', ''),
connectionType: 'default',
connectionString: '',
connectionHost: '',
connectionCert: '',
connectionMacaroon: '',
alias: '',
autopilot: true,
password: '',
startingLnd: false,
@ -493,9 +510,7 @@ const initialState = {
signupForm: {
create: true,
import: false
},
autopilot: store.get('autopilot', true)
}
}
// ------------------------------------

3
package.json

@ -280,6 +280,7 @@
"axios": "^0.16.2",
"bitcoinjs-lib": "^3.2.0",
"copy-to-clipboard": "^3.0.8",
"debug": "^3.1.0",
"debug-logger": "^0.4.1",
"devtron": "^1.4.0",
"electron": "^2.0.5",
@ -290,6 +291,7 @@
"history": "^4.7.2",
"javascript-state-machine": "^3.1.0",
"lodash.get": "^4.4.2",
"lodash.pick": "^4.4.0",
"moment": "^2.22.2",
"prop-types": "^15.6.2",
"ps-node": "^0.1.6",
@ -311,6 +313,7 @@
"satoshi-bitcoin": "^1.0.4",
"source-map-support": "^0.5.6",
"split2": "^2.2.0",
"tildify": "^1.2.0",
"untildify": "^3.0.3",
"validator": "^10.4.0"
},

1
test/unit/__mocks__/grpc.js

@ -0,0 +1 @@
module.exports = {}

208
test/unit/lnd/lnd-config.spec.js

@ -0,0 +1,208 @@
// @flow
import { join, normalize } from 'path'
import Store from 'electron-store'
import LndConfig from 'lib/lnd/config'
jest.mock('grpc')
jest.mock('electron', () => {
const { normalize } = require('path')
return {
app: {
getPath: name => normalize(`/tmp/zap-test/${name}`),
getAppPath: () => normalize('/tmp/zap-test')
}
}
})
jest.mock('lib/lnd/util', () => {
const { normalize } = require('path')
return {
...jest.requireActual('lib/lnd/util'),
appRootPath: normalize('/tmp/zap-test/app/root'),
binaryName: 'binaryName',
binaryPath: 'binaryPath'
}
})
Store.prototype.set = jest.fn()
describe('LndConfig', function() {
const checkForStaticProperties = () => {
it('should have "binaryPath" set to the value returned by lib/lnd/util', () => {
expect(this.lndConfig.binaryPath).toEqual('binaryPath')
})
it('should have "configPath" set to "resources/lnd.conf" relative to app root from lib/lnd/util"', () => {
expect(this.lndConfig.configPath).toEqual(
normalize('/tmp/zap-test/app/root/resources/lnd.conf')
)
})
it('should have "rpcProtoPath" set to "resources/rcp.proto" relative to app root from lib/lnd/util"', () => {
expect(this.lndConfig.rpcProtoPath).toEqual(
normalize('/tmp/zap-test/app/root/resources/rpc.proto')
)
})
}
const checkForConfigProperties = type => {
it(`should have the "type" property set to the ${type} value`, () => {
expect(this.lndConfig.type).toEqual(this.type)
})
it(`should have the "currency" property set to the ${type} value`, () => {
expect(this.lndConfig.currency).toEqual(this.currency)
})
it(`should have the "network" property set to the ${type}`, () => {
expect(this.lndConfig.network).toEqual(this.network)
})
it(`should have the "wallet" property set to the ${type}`, () => {
expect(this.lndConfig.wallet).toEqual(this.wallet)
})
it(`should have the "dataDir" set to a path derived from the config, under the app userData dir`, () => {
const baseDir = '/tmp/zap-test/userData/lnd/'
const expectedDataDir = join(baseDir, this.currency, this.network, this.wallet)
expect(this.lndConfig.dataDir).toEqual(expectedDataDir)
})
}
const checkForLoadedProperties = () => {
it(`should have the "host" property set to the default value`, () => {
expect(this.lndConfig.host).toEqual(this.host)
})
it('should have the "cert" property set to a path relative to the datadir', () => {
expect(this.lndConfig.cert).toEqual(this.cert)
})
it('should have the "macaroon" property set to a path relative to the datadir', () => {
expect(this.lndConfig.macaroon).toEqual(this.macaroon)
})
}
const checkForSaveBehaviour = expectedData => {
it('should save the config to a file', () => {
expect(Store.prototype.set).toHaveBeenCalledWith(
`${this.type}.${this.currency}.${this.network}.${this.wallet}`,
expectedData
)
})
}
describe('"local" type', () => {
describe('New config with default options', () => {
beforeAll(() => {
this.type = 'local'
this.currency = 'bitcoin'
this.network = 'testnet'
this.wallet = 'wallet-1'
this.lndConfig = new LndConfig()
this.host = 'localhost:10009'
this.cert = join(this.lndConfig.dataDir, 'tls.cert')
this.macaroon = join(this.lndConfig.dataDir, 'admin.macaroon')
})
describe('static properties', () => {
checkForStaticProperties()
})
describe('config properties', () => {
checkForConfigProperties('default')
})
describe('.load()', () => {
beforeAll(() => this.lndConfig.load())
checkForLoadedProperties()
})
describe('.save() - no settings', () => {
beforeAll(() => this.lndConfig.save())
checkForSaveBehaviour({})
})
describe('.save() - with settings', () => {
beforeAll(() => {
this.lndConfig.alias = 'some-alias1'
this.lndConfig.autopilot = true
this.lndConfig.save()
})
checkForSaveBehaviour({ alias: 'some-alias1', autopilot: true })
})
})
describe('New config with provided options', () => {
beforeAll(() => {
this.type = 'local'
this.currency = 'litecoin'
this.network = 'mainnet'
this.wallet = 'wallet-2'
this.lndConfig = new LndConfig({
type: this.type,
currency: this.currency,
network: this.network,
wallet: this.wallet
})
this.host = 'localhost:10009'
this.cert = join(this.lndConfig.dataDir, 'tls.cert')
this.macaroon = join(this.lndConfig.dataDir, 'admin.macaroon')
})
describe('static properties', () => {
checkForStaticProperties()
})
describe('config properties', () => {
checkForConfigProperties('provided')
})
describe('.load()', () => {
beforeAll(() => this.lndConfig.load())
checkForLoadedProperties()
})
describe('.save() - no settings', () => {
beforeAll(() => this.lndConfig.save())
checkForSaveBehaviour({})
})
describe('.save() - with settings', () => {
beforeAll(() => {
this.lndConfig.alias = 'some-alias2'
this.lndConfig.autopilot = true
this.lndConfig.save()
})
checkForSaveBehaviour({ alias: 'some-alias2', autopilot: true })
})
})
describe('New config with provided options and initial configuration', () => {
beforeAll(() => {
this.type = 'custom'
this.currency = 'bitcoin'
this.network = 'testnet'
this.wallet = 'wallet-1'
this.host = 'some-host'
this.cert = 'some-cert'
this.macaroon = 'some-macaroon'
this.lndConfig = new LndConfig({
type: this.type,
currency: this.currency,
network: this.network,
wallet: this.wallet,
settings: {
host: this.host,
cert: this.cert,
macaroon: this.macaroon
}
})
})
describe('static properties', () => {
checkForStaticProperties()
})
describe('config properties', () => {
checkForConfigProperties('provided')
})
describe('.save()', () => {
beforeAll(() => this.lndConfig.save())
checkForSaveBehaviour({ host: 'some-host', cert: 'some-cert', macaroon: 'some-macaroon' })
})
})
})
})

8
yarn.lock

@ -7582,7 +7582,7 @@ lodash.omit@4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60"
lodash.pick@4.4.0:
lodash.pick@4.4.0, lodash.pick@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
@ -11650,6 +11650,12 @@ through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
tildify@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.2.0.tgz#dcec03f55dca9b7aa3e5b04f21817eb56e63588a"
dependencies:
os-homedir "^1.0.0"
time-fix-plugin@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/time-fix-plugin/-/time-fix-plugin-2.0.3.tgz#b6b1ead519099bc621e28edb77dac7531918b7e1"

Loading…
Cancel
Save