Browse Source
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
15 changed files with 669 additions and 170 deletions
@ -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 |
|||
|
@ -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 |
|||
} |
@ -0,0 +1 @@ |
|||
module.exports = {} |
@ -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' }) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
Loading…
Reference in new issue