Browse Source

Merge pull request #866 from mrfelton/feat/indexeddb-dexie

feat(settings): use indexedDb for persistent data
renovate/lint-staged-8.x
JimmyMow 6 years ago
committed by GitHub
parent
commit
dc01b1ad37
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 62
      app/containers/Root.js
  2. 14
      app/index.js
  3. 24
      app/lib/i18n/index.js
  4. 105
      app/lib/lnd/config.js
  5. 70
      app/lib/zap/controller.js
  6. 3
      app/lib/zap/menuBuilder.js
  7. 80
      app/main.dev.js
  8. 21
      app/reducers/address.js
  9. 12
      app/reducers/channels.js
  10. 10
      app/reducers/info.js
  11. 12
      app/reducers/invoice.js
  12. 20
      app/reducers/lnd.js
  13. 15
      app/reducers/loading.js
  14. 32
      app/reducers/locale.js
  15. 100
      app/reducers/onboarding.js
  16. 26
      app/reducers/theme.js
  17. 12
      app/reducers/ticker.js
  18. 72
      app/store/db.js
  19. 5
      app/themes/index.js
  20. 3
      package.json
  21. 6
      test/unit/__mocks__/dexie.js
  22. 1
      test/unit/lnd/lightning.spec.js
  23. 125
      test/unit/lnd/lnd-config.spec.js
  24. 1
      test/unit/lnd/neutrino.spec.js
  25. 4
      test/unit/reducers/ticker.spec.js
  26. 4
      test/unit/zap/controller.spec.js
  27. 84
      yarn.lock

62
app/containers/Root.js

@ -6,8 +6,9 @@ import { ConnectedRouter } from 'connected-react-router'
import { ThemeProvider } from 'styled-components'
import { clearError, errorSelectors } from 'reducers/error'
import { loadingSelectors, setLoading } from 'reducers/loading'
import { themeSelectors } from 'reducers/theme'
import { loadingSelectors, setLoading, setMounted } from 'reducers/loading'
import { initCurrency, initLocale } from 'reducers/locale'
import { initTheme, themeSelectors } from 'reducers/theme'
import Page from 'components/UI/Page'
import Titlebar from 'components/UI/Titlebar'
@ -31,11 +32,16 @@ const SPLASH_SCREEN_TIME = 1500
class Root extends React.Component {
static propTypes = {
clearError: PropTypes.func.isRequired,
currentThemeSettings: PropTypes.object.isRequired,
theme: PropTypes.object,
error: PropTypes.string,
history: PropTypes.object.isRequired,
initLocale: PropTypes.func.isRequired,
initCurrency: PropTypes.func.isRequired,
initTheme: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
setLoading: PropTypes.func.isRequired
isMounted: PropTypes.bool.isRequired,
setLoading: PropTypes.func.isRequired,
setMounted: PropTypes.func.isRequired
}
/**
@ -52,9 +58,29 @@ class Root extends React.Component {
* // Show the loading bold briefly before showing the user the app.
*/
componentDidMount() {
const { setLoading } = this.props
const timer = setTimeout(() => setLoading(false), SPLASH_SCREEN_TIME)
this.setState({ timer })
const {
initLocale,
initCurrency,
initTheme,
isLoading,
isMounted,
setLoading,
setMounted
} = this.props
// If this is the first time the app has mounted, initialise things.
if (!isMounted) {
initTheme()
initLocale()
initCurrency()
setMounted(true)
}
// Hide the loading screen after a set time.
if (isLoading) {
const timer = setTimeout(() => setLoading(false), SPLASH_SCREEN_TIME)
this.setState({ timer })
}
}
/**
@ -66,10 +92,16 @@ class Root extends React.Component {
}
render() {
const { clearError, currentThemeSettings, error, history, isLoading } = this.props
const { clearError, theme, error, history, isLoading } = this.props
// Wait until we have determined the user's theme preference before displaying the app.
if (!theme) {
return null
}
return (
<ConnectedRouter history={history}>
<ThemeProvider theme={currentThemeSettings}>
<ThemeProvider theme={theme}>
<React.Fragment>
<GlobalStyle />
<Titlebar />
@ -90,15 +122,19 @@ class Root extends React.Component {
}
const mapStateToProps = state => ({
currentTheme: themeSelectors.currentTheme(state),
currentThemeSettings: themeSelectors.currentThemeSettings(state),
theme: themeSelectors.currentThemeSettings(state),
error: errorSelectors.getErrorState(state),
isLoading: loadingSelectors.isLoading(state)
isLoading: loadingSelectors.isLoading(state),
isMounted: loadingSelectors.isMounted(state)
})
const mapDispatchToProps = {
clearError,
setLoading
initCurrency,
initLocale,
initTheme,
setLoading,
setMounted
}
export default connect(

14
app/index.js

@ -3,8 +3,9 @@ import ReactDOM from 'react-dom'
import { Provider } from 'react-intl-redux'
import jstz from 'jstimezonedetect'
import { configureStore, history } from './store/configureStore'
import { getLocale } from './lib/i18n'
import { getDefaultLocale } from './lib/i18n'
import Root from './containers/Root'
import db from './store/db'
// Load global styles.
import './styles/app.global.scss'
@ -15,14 +16,17 @@ import './lib/i18n/locale'
// Get translations.
import translations from './lib/i18n/translation'
// Determine the users current locale.
const locale = getLocale()
// Initialise the database.
db.open()
// Default the locale to English.
const defaultLocale = getDefaultLocale()
// Initialise the intl store with data from the users current locale.
const initialState = {
intl: {
locale,
messages: translations[locale],
locale: defaultLocale,
messages: translations[defaultLocale],
timeZone: jstz.determine().name()
}
}

24
app/lib/i18n/index.js

@ -1,5 +1,3 @@
import { app, remote } from 'electron'
import Store from 'electron-store'
import get from 'lodash.get'
import { lookup } from 'country-data-lookup'
import createDebug from 'debug'
@ -68,21 +66,15 @@ const debug = (...args) => {
* Get the most appropriate language code.
* @return {string} Language code.
*/
export const getLocale = () => {
const store = new Store({ name: 'settings' })
const userLocale = store.get('locale')
if (userLocale) {
debug('Determined locale as %s from settings', userLocale)
return userLocale
}
const defaultLocale = (app || remote.app).getLocale() || 'en-US'
export const getDefaultLocale = () => {
const defaultLocale = window.navigator.language || 'en-US'
const language = defaultLocale.toLowerCase().split(/[_-]+/)[0]
let locale = 'en'
if (locales.includes(language)) {
locale = language
}
if (locales.includes(defaultLocale)) {
locale = userLocale
locale = defaultLocale
}
debug('Determined locale as %s', locale)
return locale
@ -113,14 +105,8 @@ export const getLanguageName = lang => {
* Get the most appropriate currency code.
* @return {string} Currency code.
*/
export const getCurrency = () => {
const store = new Store({ name: 'settings' })
const userCurrency = store.get('fiatTicker')
if (userCurrency) {
debug('Determined currency as %s from settings', userCurrency)
return userCurrency
}
const defaultLocale = (app || remote.app).getLocale() || 'en-US'
export const getDefaultCurrency = () => {
const defaultLocale = getDefaultLocale()
const country = defaultLocale.split(/[_-]+/)[1]
const data = lookup.countries({ alpha2: country })
const detectedCurrency = get(data, '[0]currencies[0]', 'USD')

105
app/lib/lnd/config.js

@ -2,8 +2,6 @@
import { join } from 'path'
import { app } from 'electron'
import Store from 'electron-store'
import pick from 'lodash.pick'
import createDebug from 'debug'
import untildify from 'untildify'
import tildify from 'tildify'
@ -12,20 +10,20 @@ import { appRootPath, binaryPath } from './util'
const debug = createDebug('zap:lnd-config')
// Supported connection types.
const types = {
export const types = {
local: 'Local',
custom: 'Custom',
btcpayserver: 'BTCPay Server'
}
// Supported currencies.
const currencties = {
// Supported chains.
export const chains = {
bitcoin: 'Bitcoin',
litecoin: 'Litecoin'
}
// Supported networks.
const networks = {
export const networks = {
mainnet: 'Mainnet',
testnet: 'Testnet'
}
@ -52,10 +50,11 @@ type LndConfigSettingsBtcPayServerType = {|
// Type definition for for BTCPay Server connection settings.
type LndConfigSettingsType = {|
id?: number,
wallet?: string,
type: $Keys<typeof types>,
currency: $Keys<typeof currencties>,
network: $Keys<typeof networks>,
wallet: string
chain: $Keys<typeof chains>,
network: $Keys<typeof networks>
|}
// Type definition for LndConfig constructor options.
@ -83,24 +82,17 @@ const safeUntildify = <T>(val: ?T): ?T => (typeof val === 'string' ? untildify(v
* LndConfig class
*/
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.
id: number
type: string
currency: string
chain: string
network: string
wallet: string
// User configurable settings.
host: ?string
@ -111,7 +103,7 @@ class LndConfig {
autopilot: ?boolean
// Read only data properties.
+key: string
+wallet: string
+binaryPath: string
+lndDir: string
+configPath: string
@ -122,9 +114,8 @@ class LndConfig {
*
* @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.chain config chain (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.
*/
constructor(options?: LndConfigOptions) {
@ -134,9 +125,12 @@ class LndConfig {
// flow currently doesn't support defineProperties properly (https://github.com/facebook/flow/issues/285)
const { defineProperties } = Object
defineProperties(this, {
key: {
wallet: {
enumerable: true,
get() {
return `${this.type}.${this.currency}.${this.network}.${this.wallet}`
if (this.type === 'local') {
return `wallet-${this.id}`
}
}
},
binaryPath: {
@ -148,7 +142,9 @@ class LndConfig {
lndDir: {
enumerable: true,
get() {
return join(app.getPath('userData'), 'lnd', this.currency, this.network, this.wallet)
if (this.type === 'local') {
return join(app.getPath('userData'), 'lnd', this.chain, this.network, this.wallet)
}
}
},
configPath: {
@ -185,7 +181,7 @@ class LndConfig {
return safeUntildify(_cert.get(this))
},
set(value: string) {
_cert.set(this, safeTrim(value))
_cert.set(this, safeTildify(safeTrim(value)))
}
},
@ -198,7 +194,7 @@ class LndConfig {
return safeUntildify(_macaroon.get(this))
},
set(value: string) {
_macaroon.set(this, safeTrim(value))
_macaroon.set(this, safeTildify(safeTrim(value)))
}
},
@ -217,10 +213,12 @@ class LndConfig {
// If options were provided, use them to initialise the instance.
if (options) {
if (options.id) {
this.id = options.id
}
this.type = options.type
this.currency = options.currency
this.chain = options.chain
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) {
@ -229,66 +227,17 @@ class LndConfig {
}
}
// 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.lndDir, 'tls.cert'),
macaroon: join(this.lndDir, 'data', 'chain', this.currency, this.network, 'admin.macaroon')
macaroon: join(this.lndDir, 'data', 'chain', this.chain, this.network, 'admin.macaroon')
}
debug('Connection type is local. Assigning settings as: %o', defaultLocalOptions)
Object.assign(this, defaultLocalOptions)
}
}
/**
* 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 LndConfig

70
app/lib/zap/controller.js

@ -2,17 +2,19 @@
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 LndConfig from '../lnd/config'
import LndConfig, { chains, networks, types } from '../lnd/config'
import Lightning from '../lnd/lightning'
import Neutrino from '../lnd/neutrino'
import WalletUnlocker from '../lnd/walletUnlocker'
type onboardingOptions = {
type: 'local' | 'custom' | 'btcpayserver',
id?: number,
type: $Keys<typeof types>,
chain?: $Keys<typeof chains>,
network?: $Keys<typeof networks>,
host?: string,
cert?: string,
macaroon?: string,
@ -65,15 +67,15 @@ class ZapController {
this.fsm = new StateMachine({
transitions: [
{ name: 'startOnboarding', from: '*', to: 'onboarding' },
{ name: 'startLnd', from: 'onboarding', to: 'running' },
{ name: 'connectLnd', from: 'onboarding', to: 'connected' },
{ name: 'startLocalLnd', from: 'onboarding', to: 'running' },
{ name: 'startRemoteLnd', from: 'onboarding', to: 'connected' },
{ name: 'terminate', from: '*', to: 'terminated' }
],
methods: {
onOnboarding: this.onOnboarding.bind(this),
onStartOnboarding: this.onStartOnboarding.bind(this),
onBeforeStartLnd: this.onBeforeStartLnd.bind(this),
onBeforeConnectLnd: this.onBeforeConnectLnd.bind(this),
onBeforeStartLocalLnd: this.onBeforeStartLocalLnd.bind(this),
onBeforeStartRemoteLnd: this.onBeforeStartRemoteLnd.bind(this),
onTerminated: this.onTerminated.bind(this),
onTerminate: this.onTerminate.bind(this)
}
@ -81,10 +83,6 @@ class ZapController {
// Variable to hold the main window instance.
this.mainWindow = mainWindow
// Initialise the controler with the current active config.
this.lndConfig = new LndConfig()
this.lndConfig.load()
}
/**
@ -130,11 +128,11 @@ class ZapController {
startOnboarding(...args: any[]) {
return this.fsm.startOnboarding(...args)
}
startLnd(...args: any[]) {
return this.fsm.startLnd(...args)
startLocalLnd(...args: any[]) {
return this.fsm.startLocalLnd(...args)
}
connectLnd(...args: any[]) {
return this.fsm.connectLnd(...args)
startRemoteLnd(...args: any[]) {
return this.fsm.startRemoteLnd(...args)
}
terminate(...args: any[]) {
return this.fsm.terminate(...args)
@ -177,11 +175,11 @@ class ZapController {
mainLog.debug('[FSM] onStartOnboarding...')
// Notify the app to start the onboarding process.
this.sendMessage('startOnboarding', this.lndConfig)
this.sendMessage('startOnboarding')
}
onBeforeStartLnd() {
mainLog.debug('[FSM] onBeforeStartLnd...')
onBeforeStartLocalLnd() {
mainLog.debug('[FSM] onBeforeStartLocalLnd...')
mainLog.info('Starting new lnd instance')
mainLog.info(' > alias:', this.lndConfig.alias)
@ -190,8 +188,8 @@ class ZapController {
return this.startNeutrino()
}
onBeforeConnectLnd() {
mainLog.debug('[FSM] onBeforeConnectLnd...')
onBeforeStartRemoteLnd() {
mainLog.debug('[FSM] onBeforeStartRemoteLnd...')
mainLog.info('Connecting to custom lnd instance')
mainLog.info(' > host:', this.lndConfig.host)
mainLog.info(' > cert:', this.lndConfig.cert)
@ -288,7 +286,7 @@ class ZapController {
)
// Notify the renderer that the wallet unlocker is active.
this.sendMessage('walletUnlockerGrpcActive')
this.sendMessage('walletUnlockerGrpcActive', this.lndConfig)
} catch (err) {
mainLog.warn('Unable to connect to WalletUnlocker gRPC interface: %o', err)
throw err
@ -312,7 +310,7 @@ class ZapController {
ipcMain.on('lnd', (event, { msg, data }) => this.lightning.registerMethods(event, msg, data))
// Let the renderer know that we are connected.
this.sendMessage('lightningGrpcActive')
this.sendMessage('lightningGrpcActive', this.lndConfig)
} catch (err) {
mainLog.warn('Unable to connect to Lightning gRPC interface: %o', err)
throw err
@ -425,27 +423,17 @@ class ZapController {
/**
* Start or connect to lnd process after onboarding has been completed by the app.
*/
finishOnboarding(options: onboardingOptions) {
mainLog.info('Finishing onboarding')
async startLnd(options: onboardingOptions) {
mainLog.info('Starting lnd with options: %o', options)
// 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',
id: options.id,
type: options.type || 'local',
chain: options.chain || 'bitcoin',
network: options.network || 'testnet',
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 =
@ -453,14 +441,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.
return options.type === 'local' ? this.startLnd() : this.connectLnd()
return options.type === 'local' ? this.startLocalLnd() : this.startRemoteLnd()
}
/**
* Add IPC event listeners...
*/
_registerIpcListeners() {
ipcMain.on('startLnd', (event, options: onboardingOptions) => this.finishOnboarding(options))
ipcMain.on('startLnd', (event, options: onboardingOptions) => this.startLnd(options))
ipcMain.on('startLightningWallet', () =>
this.startLightningWallet().catch(e => {
// Notify the app of errors.

3
app/lib/zap/menuBuilder.js

@ -1,6 +1,6 @@
// @flow
import { app, Menu, shell, BrowserWindow, ipcMain } from 'electron'
import { getLocale, getLanguageName, locales } from '../i18n'
import { getLanguageName, locales } from '../i18n'
export default class ZapMenuBuilder {
mainWindow: BrowserWindow
@ -8,7 +8,6 @@ export default class ZapMenuBuilder {
constructor(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow
this.locale = getLocale()
ipcMain.on('setLocale', (event, locale) => this.buildMenu(locale))
}

80
app/main.dev.js

@ -11,20 +11,94 @@ import installExtension, {
REACT_DEVELOPER_TOOLS,
REDUX_DEVTOOLS
} from 'electron-devtools-installer'
import get from 'lodash.get'
import { mainLog } from './lib/utils/log'
import ZapMenuBuilder from './lib/zap/menuBuilder'
import ZapController from './lib/zap/controller'
import ZapUpdater from './lib/zap/updater'
import themes from './themes'
import { getDbName } from './store/db'
// Set up a couple of timers to track the app startup progress.
mainLog.time('Time until app is ready')
/**
* Fetch user settings from indexedDb.
* We do this by starting up a new browser window and accessing indexedDb from within it.
*
* @return {[type]} 'settings' store from indexedDb.
*/
const fetchSettings = () => {
const win = new BrowserWindow({ show: false })
if (process.env.HOT) {
const port = process.env.PORT || 1212
win.loadURL(`http://localhost:${port}`)
} else {
win.loadURL(`file://${__dirname}`)
}
const dbName = getDbName()
mainLog.debug(`Fetching user settings from indexedDb (using database "%s")`, dbName)
return win.webContents
.executeJavaScript(
`
new Promise((resolve, reject) => {
var DBOpenRequest = window.indexedDB.open('${dbName}')
DBOpenRequest.onupgradeneeded = function(event) {
event.target.transaction.abort()
return reject(new Error('Database does not exist'))
}
DBOpenRequest.onerror = function() {
return reject(new Error('Error loading database'))
}
DBOpenRequest.onsuccess = function() {
const db = DBOpenRequest.result
var transaction = db.transaction(['settings'], 'readwrite')
transaction.onerror = function() {
return reject(transaction.error)
}
var objectStore = transaction.objectStore('settings')
var objectStoreRequest = objectStore.getAll()
objectStoreRequest.onsuccess = function() {
return resolve(objectStoreRequest.result)
}
}
})
`
)
.then(res => {
mainLog.debug('Got user settings: %o', res)
return res
})
}
const getSetting = (store, key) => {
const setting = store.find(s => s.key === key)
return setting && setting.hasOwnProperty('value') ? setting.value : null
}
/**
* Initialize Zap as soon as electron is ready.
*/
app.on('ready', () => {
app.on('ready', async () => {
mainLog.timeEnd('Time until app is ready')
// Get the users preference so that we can:
// - set the background colour of the window to avoid unwanted flicker.
// - Initialise the Language menu with the users locale selected by default.
let theme = {}
let locale
try {
const settings = await fetchSettings()
locale = getSetting(settings, 'locale')
const themeKey = getSetting(settings, 'theme')
theme = themes[themeKey]
} catch (e) {
mainLog.warn('Unable to determine user locale and theme', e)
}
// Create a new browser window.
const mainWindow = new BrowserWindow({
show: false,
@ -34,7 +108,7 @@ app.on('ready', () => {
height: 600,
minWidth: 950,
minHeight: 425,
backgroundColor: '#1c1e26'
backgroundColor: get(theme, 'colors.darkestBackground', '#242633')
})
// Initialise the updater.
@ -47,7 +121,7 @@ app.on('ready', () => {
// Initialise the application menus.
const menuBuilder = new ZapMenuBuilder(mainWindow)
menuBuilder.buildMenu()
menuBuilder.buildMenu(locale)
/**
* In production mode, enable source map support.

21
app/reducers/address.js

@ -1,5 +1,5 @@
import { ipcRenderer } from 'electron'
import Store from 'electron-store'
import db from 'store/db'
// ------------------------------------
// Constants
@ -39,15 +39,18 @@ export function closeWalletModal() {
}
// Get our existing address if there is one, otherwise generate a new one.
export const walletAddress = type => (dispatch, getState) => {
export const walletAddress = type => async (dispatch, getState) => {
let address
// Wallet addresses are keyed under the node pubKey in our store.
const state = getState()
const pubKey = state.info.data.identity_pubkey
if (pubKey) {
const store = new Store({ name: 'wallet' })
address = store.get(`${pubKey}.${type}`, null)
const node = await db.nodes.get({ id: pubKey })
if (node) {
address = node.getCurrentAddress(type)
}
}
// If we have an address already, use that. Otherwise, generate a new address.
@ -65,15 +68,19 @@ export const newAddress = type => dispatch => {
}
// Receive IPC event for info
export const receiveAddress = (event, data) => (dispatch, getState) => {
export const receiveAddress = (event, data) => async (dispatch, getState) => {
const state = getState()
const pubKey = state.info.data.identity_pubkey
// If we know the node's public key, store the address for reuse.
if (pubKey) {
const type = Object.keys(addressTypes).find(key => addressTypes[key] === data.type)
const store = new Store({ name: 'wallet' })
store.set(`${pubKey}.${type}`, data.address)
const node = await db.nodes.get(pubKey)
if (node) {
await node.setCurrentAddress(type, data.address)
} else {
await db.nodes.put({ id: pubKey, addresses: { [type]: data.address } })
}
}
dispatch({ type: RECEIVE_ADDRESS, address: data.address })

12
app/reducers/channels.js

@ -1,9 +1,9 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import Store from 'electron-store'
import { btc } from 'lib/utils'
import { showNotification } from 'lib/utils/notifications'
import { requestSuggestedNodes } from 'lib/utils/api'
import db from 'store/db'
import { setError } from './error'
// ------------------------------------
@ -179,22 +179,22 @@ export const receiveChannels = (event, { channels, pendingChannels }) => dispatc
dispatch({ type: RECEIVE_CHANNELS, channels, pendingChannels })
// Send IPC event for opening a channel
export const openChannel = ({ pubkey, host, local_amt }) => (dispatch, getState) => {
export const openChannel = ({ pubkey, host, local_amt }) => async (dispatch, getState) => {
const state = getState()
const localamt = btc.convert(state.ticker.currency, 'sats', local_amt)
dispatch(openingChannel())
dispatch(addLoadingPubkey(pubkey))
// Grab the activeConnection type from our local store. If the active connection type is local (light clients using
// Grab the activeWallet type from our local store. If the active connection type is local (light clients using
// neutrino) we will flag manually created channels as private. Other connections like remote node and BTCPay Server
// we will announce to the network as these users are using Zap to drive nodes that are online 24/7
const store = new Store({ name: 'settings' })
const { type } = store.get('activeConnection', {})
const activeWallet = await db.settings.get({ key: 'activeWallet' })
const wallet = (await db.wallets.get({ id: activeWallet.value })) || {}
ipcRenderer.send('lnd', {
msg: 'connectAndOpen',
data: { pubkey, host, localamt, private: type === 'local' }
data: { pubkey, host, localamt, private: wallet.type === 'local' }
})
}

10
app/reducers/info.js

@ -1,7 +1,6 @@
import Store from 'electron-store'
import bitcoin from 'bitcoinjs-lib'
import { ipcRenderer } from 'electron'
import db from 'store/db'
import { walletAddress } from './address'
// ------------------------------------
@ -42,13 +41,12 @@ export const fetchInfo = () => async dispatch => {
}
// Receive IPC event for info
export const receiveInfo = (event, data) => (dispatch, getState) => {
export const receiveInfo = (event, data) => async (dispatch, getState) => {
// Determine the node's current sync state.
const state = getState()
if (typeof state.info.hasSynced === 'undefined') {
const store = new Store({ name: 'wallet' })
const hasSynced = store.get(`${data.identity_pubkey}.hasSynced`, false)
store.set(`${data.identity_pubkey}.hasSynced`, hasSynced)
const node = await db.nodes.get({ id: data.identity_pubkey })
const hasSynced = node ? node.hasSynced : false
dispatch(setHasSynced(hasSynced))
}

12
app/reducers/invoice.js

@ -1,7 +1,7 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import { push } from 'react-router-redux'
import Store from 'electron-store'
import db from 'store/db'
import { showNotification } from 'lib/utils/notifications'
import { btc } from 'lib/utils'
@ -111,22 +111,22 @@ export const receiveInvoices = (event, { invoices }) => dispatch => {
}
// Send IPC event for creating an invoice
export const createInvoice = (amount, memo, currency) => dispatch => {
export const createInvoice = (amount, memo, currency) => async dispatch => {
// backend needs value in satoshis no matter what currency we are using
const value = btc.convert(currency, 'sats', amount)
dispatch(sendInvoice())
// Grab the activeConnection type from our local store. If the active connection type is local (light clients using
// Grab the activeWallet type from our local store. If the active connection type is local (light clients using
// neutrino) we will have to flag private as true when creating this invoice. All light cliets open private channels
// (both manual and autopilot ones). In order for these clients to receive money through these channels the invoices
// need to come with routing hints for private channels
const store = new Store({ name: 'settings' })
const { type } = store.get('activeConnection', {})
const activeWallet = await db.settings.get({ key: 'activeWallet' })
const wallet = db.wallets.get({ id: activeWallet.value })
ipcRenderer.send('lnd', {
msg: 'createInvoice',
data: { value, memo, private: type === 'local' }
data: { value, memo, private: wallet.type === 'local' }
})
}

20
app/reducers/lnd.js

@ -1,9 +1,9 @@
import Store from 'electron-store'
import { createSelector } from 'reselect'
import { showNotification } from 'lib/utils/notifications'
import db from 'store/db'
import { fetchTicker } from './ticker'
import { fetchBalance } from './balance'
import { fetchInfo, setHasSynced } from './info'
import { fetchInfo, setHasSynced, infoSelectors } from './info'
import { lndWalletStarted, lndWalletUnlockerStarted } from './onboarding'
// ------------------------------------
@ -26,16 +26,20 @@ export const SET_LIGHTNING_WALLET_ACTIVE = 'SET_LIGHTNING_WALLET_ACTIVE'
// ------------------------------------
// Receive IPC event for LND sync status change.
export const lndSyncStatus = (event, status) => (dispatch, getState) => {
export const lndSyncStatus = (event, status) => async (dispatch, getState) => {
const notifTitle = 'Lightning Node Synced'
const notifBody = "Visa who? You're your own payment processor now!"
// Persist the fact that the wallet has been synced at least once.
const state = getState()
const pubKey = state.info.data.identity_pubkey
if (pubKey) {
const store = new Store({ name: 'wallet' })
store.set(`${pubKey}.hasSynced`, true)
const hasSynced = infoSelectors.hasSynced(state)
if (pubKey && !hasSynced) {
const updated = await db.nodes.update(pubKey, { hasSynced: true })
if (!updated) {
await db.nodes.add({ id: pubKey, hasSynced: true })
}
}
switch (status) {
@ -64,11 +68,11 @@ export const lndSyncStatus = (event, status) => (dispatch, getState) => {
}
// Connected to Lightning gRPC interface (lnd wallet is connected and unlocked)
export const lightningGrpcActive = () => dispatch => {
export const lightningGrpcActive = (event, lndConfig) => dispatch => {
dispatch({ type: SET_LIGHTNING_WALLET_ACTIVE })
// Let the onboarding process know that wallet is active.
dispatch(lndWalletStarted())
dispatch(lndWalletStarted(lndConfig))
}
// Connected to WalletUnlocker gRPC interface (lnd is ready to unlock or create wallet)

15
app/reducers/loading.js

@ -2,13 +2,15 @@
// Initial State
// ------------------------------------
const initialState = {
isLoading: true
isLoading: true,
isMounted: false
}
// ------------------------------------
// Constants
// ------------------------------------
export const SET_LOADING = 'SET_LOADING'
export const SET_MOUNTED = 'SET_MOUNTED'
// ------------------------------------
// Actions
@ -20,11 +22,19 @@ export function setLoading(isLoading) {
}
}
export function setMounted(isMounted) {
return {
type: SET_MOUNTED,
isMounted
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[SET_LOADING]: (state, { isLoading }) => ({ ...state, isLoading })
[SET_LOADING]: (state, { isLoading }) => ({ ...state, isLoading }),
[SET_MOUNTED]: (state, { isMounted }) => ({ ...state, isMounted })
}
// ------------------------------------
@ -33,6 +43,7 @@ const ACTION_HANDLERS = {
const loadingSelectors = {}
loadingSelectors.isLoading = state => state.loading.isLoading
loadingSelectors.isMounted = state => state.loading.isMounted
export { loadingSelectors }

32
app/reducers/locale.js

@ -1,10 +1,8 @@
import Store from 'electron-store'
import { updateIntl } from 'react-intl-redux'
import { ipcRenderer } from 'electron'
import translations from 'lib/i18n/translation'
// Settings store
const store = new Store({ name: 'settings' })
import db from 'store/db'
import { setFiatTicker } from './ticker'
// ------------------------------------
// Actions
@ -22,7 +20,7 @@ export const setLocale = locale => (dispatch, getState) => {
)
// Save the new locale sa our language preference.
store.set('locale', locale)
db.settings.put({ key: 'locale', value: locale })
// Let the main process know the locale has changed.
ipcRenderer.send('setLocale', locale)
@ -32,6 +30,30 @@ export const receiveLocale = (event, locale) => dispatch => {
dispatch(setLocale(locale))
}
export const initLocale = () => async (dispatch, getState) => {
const userLocale = await db.settings.get({ key: 'locale' })
const state = getState()
const currentLocale = localeSelectors.currentLocale(state)
if (userLocale && userLocale.value) {
const locale = userLocale.value
if (currentLocale !== locale) {
dispatch(setLocale(locale))
}
}
}
export const initCurrency = () => async dispatch => {
const userCurrency = await db.settings.get({ key: 'fiatTicker' })
if (userCurrency && userCurrency.value) {
dispatch(setFiatTicker(userCurrency.value))
}
}
export const localeSelectors = {
currentLocale: state => state.intl.locale
}
// ------------------------------------
// Reducer
// ------------------------------------

100
app/reducers/onboarding.js

@ -1,6 +1,7 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import get from 'lodash.get'
import db from 'store/db'
import { fetchInfo } from './info'
import { setError } from './error'
@ -64,15 +65,18 @@ function prettyPrint(json) {
// ------------------------------------
// Actions
// ------------------------------------
export const setConnectionType = connectionType => (dispatch, getState) => {
export const setConnectionType = connectionType => async (dispatch, getState) => {
const previousType = connectionTypeSelector(getState())
// When changing the connection type clear out existing config.
// When changing the connection type, load any saved settings.
if (previousType !== connectionType) {
dispatch(setConnectionString(''))
dispatch(setConnectionHost(''))
dispatch(setConnectionCert(''))
dispatch(setConnectionMacaroon(''))
const wallet = (await db.wallets.get({ type: connectionType })) || {}
dispatch(setConnectionString(wallet.string || initialState.connectionString))
dispatch(setConnectionHost(wallet.host || initialState.connectionHost))
dispatch(setConnectionCert(wallet.cert || initialState.connectionCert))
dispatch(setConnectionMacaroon(wallet.macaroon || initialState.connectionMacaroon))
dispatch(updateAlias(wallet.alias || initialState.alias))
dispatch(setAutopilot(wallet.autopilot || initialState.autopilot))
dispatch(setStartLndError({}))
}
@ -180,19 +184,32 @@ export function changeStep(step) {
}
}
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)
export const startLnd = options => async dispatch => {
// Attempt to load the wallet settings.
// TODO: Currently, this only support a single wallet config per type.
let wallet = await db.wallets.get({ type: options.type })
return {
type: STARTING_LND
// If a wallet was found, merge in our user selected options and update in the db.
if (wallet) {
Object.assign(wallet, options)
await db.wallets.put(wallet)
}
}
export function lndStarted() {
return {
type: LND_STARTED
// Otherwise, save the new wallet config.
else {
const id = await db.wallets.put(options)
wallet = Object.assign(options, { id })
}
// Tell the main process to start lnd using the supplied connection details.
ipcRenderer.send('startLnd', wallet)
// Update the store.
dispatch({ type: STARTING_LND })
}
export const lndStarted = () => async dispatch => {
dispatch({ type: LND_STARTED })
}
export function setStartLndError(errors) {
@ -237,9 +254,18 @@ export const lndWalletUnlockerStarted = () => dispatch => {
* 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 => {
export const lndWalletStarted = lndConfig => async dispatch => {
// Save the wallet settings.
const walletId = await db.wallets.put(lndConfig)
// Save the active wallet config.
await db.settings.put({
key: 'activeWallet',
value: walletId
})
dispatch(fetchInfo())
dispatch(lndStarted())
dispatch(lndStarted(lndConfig))
}
export const submitNewWallet = (
@ -269,24 +295,30 @@ export const recoverOldWallet = (
}
// 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
export const startOnboarding = () => async dispatch => {
// If we have an active wallet saved, load it's settings.
const activeWallet = await db.settings.get({ key: 'activeWallet' })
if (activeWallet) {
const wallet = await db.wallets.get({ id: activeWallet.value })
if (wallet) {
dispatch(setConnectionType(wallet.type))
switch (wallet.type) {
case 'local':
dispatch(updateAlias(wallet.alias))
dispatch(setAutopilot(wallet.autopilot))
break
case 'custom':
dispatch(setConnectionHost(wallet.host))
dispatch(setConnectionCert(wallet.cert))
dispatch(setConnectionMacaroon(wallet.macaroon))
break
case 'btcpayserver':
dispatch(setConnectionString(wallet.string))
break
}
}
}
dispatch({ type: ONBOARDING_STARTED })
}

26
app/reducers/theme.js

@ -1,9 +1,6 @@
import { createSelector } from 'reselect'
import Store from 'electron-store'
import { dark, light } from 'themes'
// Settings store
const store = new Store({ name: 'settings' })
import db from 'store/db'
// ------------------------------------
// Constants
@ -16,9 +13,26 @@ const DEFAULT_THEME = 'dark'
// Actions
// ------------------------------------
export const initTheme = () => async (dispatch, getState) => {
let theme
try {
const userTheme = await db.settings.get({ key: 'theme' })
theme = userTheme.value || DEFAULT_THEME
} catch (e) {
theme = DEFAULT_THEME
}
const state = getState()
const currentTheme = themeSelectors.currentTheme(state)
if (currentTheme !== theme) {
dispatch(setTheme(theme))
}
}
export function setTheme(currentTheme) {
// Persist the new fiatTicker in our ticker store
store.set('theme', currentTheme)
db.settings.put({ key: 'theme', value: currentTheme })
return {
type: SET_THEME,
@ -56,7 +70,7 @@ export { themeSelectors }
// ------------------------------------
const initialState = {
currentTheme: store.get('theme', DEFAULT_THEME),
currentTheme: null,
themes: { dark, light }
}

12
app/reducers/ticker.js

@ -1,12 +1,9 @@
import { createSelector } from 'reselect'
import Store from 'electron-store'
import { requestTicker } from 'lib/utils/api'
import { currencies, getCurrency } from 'lib/i18n'
import { currencies, getDefaultCurrency } from 'lib/i18n'
import db from 'store/db'
import { infoSelectors } from './info'
// Settings store
const store = new Store({ name: 'settings' })
// ------------------------------------
// Constants
// ------------------------------------
@ -41,7 +38,7 @@ export function setCrypto(crypto) {
export function setFiatTicker(fiatTicker) {
// Persist the new fiatTicker in our ticker store
store.set('fiatTicker', fiatTicker)
db.settings.put({ key: 'fiatTicker', value: fiatTicker })
return {
type: SET_FIAT_TICKER,
@ -67,7 +64,6 @@ export const fetchTicker = () => async dispatch => {
dispatch(getTickers())
const btcTicker = await requestTicker()
dispatch(recieveTickers({ btcTicker }))
return btcTicker
}
@ -145,7 +141,7 @@ const initialState = {
crypto: '',
btcTicker: null,
ltcTicker: null,
fiatTicker: getCurrency(),
fiatTicker: getDefaultCurrency(),
fiatTickers: currencies,
currencyFilters: [
{

72
app/store/db.js

@ -0,0 +1,72 @@
import Dexie from 'dexie'
// Suffex the database name with NODE_ENV so that wer can have per-env databases.
export const getDbName = () => {
let name = `ZapDesktop`
if (process.env.NODE_ENV) {
name += `.${process.env.NODE_ENV}`
}
return name
}
// Define the database.
const db = new Dexie(getDbName())
db.version(1).stores({
settings: 'key',
wallets: '++id, type, chain, network',
nodes: 'id'
})
// Wallet with ID 1 will always be a local bitcoin wallet.
// This ensures that useres upgrading from older versions do not loose their initial wallet.
db.on('populate', () => {
db.wallets.add({ type: 'local', currency: 'bitcoin' })
})
/**
* @class Wallet
* Wallet helper class.
*/
export const Wallet = db.wallets.defineClass({
id: Number,
type: String,
currency: String,
network: String,
alias: String,
autopilot: Boolean,
cert: String,
host: String,
macaroon: String
})
/**
* @class Node
* Node helper class.
*/
export const Node = db.nodes.defineClass({
id: String,
hasSynced: Boolean,
addresses: Object
})
/**
* Get current address of a given type.
* @param {String} type type of address to fetch.
* @return {String} current address of requested type, if one exists.
*/
Node.prototype.getCurrentAddress = function(type) {
return Dexie.getByKeyPath(this, `addresses.${type}`)
}
/**
* Set current address of a given type.
* @param {String} type type of address to save.
* @param {String} address address to save.
* @return {Node} updated node instance.
*/
Node.prototype.setCurrentAddress = function(type, address) {
Dexie.setByKeyPath(this, `addresses.${type}`, address)
return db.nodes.put(this)
}
export default db

5
app/themes/index.js

@ -1,4 +1,7 @@
import dark from './dark'
import light from './light'
export { dark, light }
export { dark }
export { light }
export default { dark, light }

3
package.json

@ -260,6 +260,7 @@
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.11.1",
"extract-react-intl-messages": "^0.11.1",
"fake-indexeddb": "^2.0.4",
"file-loader": "^2.0.0",
"flow-bin": "^0.83.0",
"flow-typed": "^2.5.1",
@ -310,10 +311,10 @@
"country-data-lookup": "^0.0.3",
"debug": "^4.1.0",
"debug-logger": "^0.4.1",
"dexie": "^2.0.4",
"downshift": "^3.1.0",
"electron": "^3.0.4",
"electron-is-dev": "^1.0.1",
"electron-store": "^2.0.0",
"font-awesome": "^4.7.0",
"get-port": "^4.0.0",
"history": "^4.7.2",

6
test/unit/__mocks__/dexie.js

@ -0,0 +1,6 @@
import Dexie from 'dexie'
Dexie.dependencies.indexedDB = require('fake-indexeddb')
Dexie.dependencies.IDBKeyRange = require('fake-indexeddb/lib/FDBKeyRange')
module.exports = Dexie

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

@ -1,7 +1,6 @@
import { BrowserWindow } from 'electron'
import Lightning from 'lib/lnd/lightning'
jest.mock('electron-store')
jest.mock('lib/lnd/subscribe/transactions')
jest.mock('lib/lnd/subscribe/invoices')
jest.mock('lib/lnd/subscribe/channelgraph')

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

@ -1,10 +1,8 @@
// @flow
import { join, normalize } from 'path'
import Store from 'electron-store'
import LndConfig from 'lib/lnd/config'
jest.mock('electron-store')
jest.mock('lib/lnd/util', () => {
return {
...jest.requireActual('lib/lnd/util'),
@ -30,8 +28,8 @@ describe('LndConfig', function() {
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 "chain" property set to the ${type} value`, () => {
expect(this.lndConfig.chain).toEqual(this.chain)
})
it(`should have the "network" property set to the ${type}`, () => {
expect(this.lndConfig.network).toEqual(this.network)
@ -41,102 +39,25 @@ describe('LndConfig', function() {
})
it(`should have the "lndDir" 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)
const expectedDataDir = join(baseDir, this.chain, this.network, this.wallet)
expect(this.lndConfig.lndDir).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.lndDir, 'tls.cert')
this.macaroon = join(
this.lndConfig.lndDir,
'data',
'chain',
this.currency,
this.network,
'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.wallet = 'wallet-1'
this.type = 'local'
this.currency = 'litecoin'
this.chain = 'litecoin'
this.network = 'mainnet'
this.wallet = 'wallet-2'
this.lndConfig = new LndConfig({
id: 1,
type: this.type,
currency: this.currency,
network: this.network,
wallet: this.wallet
chain: this.chain,
network: this.network
})
this.host = 'localhost:10009'
this.cert = join(this.lndConfig.lndDir, 'tls.cert')
this.macaroon = join(
this.lndConfig.lndDir,
'data',
'chain',
this.currency,
this.network,
'admin.macaroon'
)
})
describe('static properties', () => {
@ -145,39 +66,23 @@ describe('LndConfig', function() {
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.type = 'local'
this.chain = 'bitcoin'
this.network = 'testnet'
this.host = 'some-host'
this.cert = 'some-cert'
this.macaroon = 'some-macaroon'
this.lndConfig = new LndConfig({
id: 1,
type: this.type,
currency: this.currency,
chain: this.chain,
network: this.network,
wallet: this.wallet,
settings: {
host: this.host,
cert: this.cert,
@ -192,10 +97,6 @@ describe('LndConfig', function() {
describe('config properties', () => {
checkForConfigProperties('provided')
})
describe('.save()', () => {
beforeAll(() => this.lndConfig.save())
checkForSaveBehaviour({ host: 'some-host', cert: 'some-cert', macaroon: 'some-macaroon' })
})
})
})
})

1
test/unit/lnd/neutrino.spec.js

@ -4,7 +4,6 @@ import Neutrino from 'lib/lnd/neutrino'
import LndConfig from 'lib/lnd/config'
import mockSpawn from 'mock-spawn'
jest.mock('electron-store')
jest.mock('child_process', () => {
var mockSpawn = require('mock-spawn')
return {

4
test/unit/reducers/ticker.spec.js

@ -1,6 +1,5 @@
// @flow
import Store from 'electron-store'
import tickerReducer, {
SET_CURRENCY,
SET_CRYPTO,
@ -8,9 +7,6 @@ import tickerReducer, {
RECIEVE_TICKERS
} from 'reducers/ticker'
Store.prototype.set = jest.fn()
Store.prototype.get = jest.fn()
describe('reducers', () => {
describe('tickerReducer', () => {
it('should handle initial state', () => {

4
test/unit/zap/controller.spec.js

@ -1,5 +1,4 @@
import ZapController from 'lib/zap/controller'
import LndConfig from 'lib/lnd/config'
import Lightning from 'lib/lnd/lightning'
jest.mock('lib/lnd/lightning')
@ -11,9 +10,6 @@ describe('ZapController', function() {
})
describe('initial values', () => {
it('should set the "lndConfig" property to a new LndConfig instance', () => {
expect(this.controller.lndConfig).toBeInstanceOf(LndConfig)
})
it('should set the "mainWindow" property to undefined', () => {
expect(this.controller.mainWindow).toBeUndefined()
})

84
yarn.lock

@ -3738,6 +3738,11 @@ base-x@^3.0.2:
dependencies:
safe-buffer "^5.0.1"
base64-arraybuffer-es6@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.3.1.tgz#fdf0e382f4e2f56caf881f48ee0ce01ae79afe48"
integrity sha512-TrhBheudYaff9adiTAqjSScjvtmClQ4vF9l4cqkPNkVsA11m4/NRdH4LkZ/tAMmpzzwfI20BXnJ/PTtafECCNA==
base64-js@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
@ -5045,17 +5050,6 @@ concurrently@^4.0.1:
tree-kill "^1.1.0"
yargs "^12.0.1"
conf@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/conf/-/conf-2.0.0.tgz#43f7e282b32faca31f4d18bf279d6841ad657d5a"
integrity sha512-iCLzBsGFi8S73EANsEJZz0JnJ/e5VZef/kSaxydYZLAvw0rFNAUx5R7K5leC/CXXR2mZfXWhUvcZOO/dM2D5xg==
dependencies:
dot-prop "^4.1.0"
env-paths "^1.0.0"
make-dir "^1.0.0"
pkg-up "^2.0.0"
write-file-atomic "^2.3.0"
config-chain@~1.1.11:
version "1.1.12"
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
@ -5184,7 +5178,7 @@ copy-to-clipboard@^3.0.8:
dependencies:
toggle-selection "^1.0.3"
core-js@2.5.7, core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.3, core-js@^2.5.7:
core-js@2.5.7, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0, core-js@^2.5.3, core-js@^2.5.7:
version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==
@ -5899,6 +5893,11 @@ dev-null@^0.1.1:
resolved "https://registry.yarnpkg.com/dev-null/-/dev-null-0.1.1.tgz#5a205ce3c2b2ef77b6238d6ba179eb74c6a0e818"
integrity sha1-WiBc48Ky73e2I41roXnrdMag6Bg=
dexie@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/dexie/-/dexie-2.0.4.tgz#6027a5e05879424e8f9979d8c14e7420f27e3a11"
integrity sha512-aQ/s1U2wHxwBKRrt2Z/mwFNHMQWhESerFsMYzE+5P5OsIe5o1kgpFMWkzKTtkvkyyEni6mWr/T4HUJuY9xIHLA==
dezalgo@^1.0.0, dezalgo@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"
@ -6271,13 +6270,6 @@ electron-publish@20.28.3:
lazy-val "^1.0.3"
mime "^2.3.1"
electron-store@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-2.0.0.tgz#1035cca2a95409d1f54c7466606345852450d64a"
integrity sha512-1WCFYHsYvZBqDsoaS0Relnz0rd81ZkBAI0Fgx7Nq2UWU77rSNs1qxm4S6uH7TCZ0bV3LQpJFk7id/is/ZgoOPA==
dependencies:
conf "^2.0.0"
electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.62, electron-to-chromium@^1.3.79:
version "1.3.80"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.80.tgz#e99ec7efe64c2c6a269d3885ff411ea88852fa53"
@ -7206,6 +7198,15 @@ eyes@0.1.x:
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=
fake-indexeddb@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-2.0.4.tgz#401715deb7fc9501866c9f329bde7742599e2de8"
integrity sha1-QBcV3rf8lQGGbJ8ym953QlmeLeg=
dependencies:
core-js "^2.4.1"
realistic-structured-clone "^2.0.1"
setimmediate "^1.0.5"
fast-deep-equal@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
@ -12633,7 +12634,7 @@ pkg-dir@^3.0.0:
dependencies:
find-up "^3.0.0"
pkg-up@2.0.0, pkg-up@^2.0.0:
pkg-up@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
integrity sha1-yBmscoBZpGHKscOImivjxJoATX8=
@ -14190,6 +14191,16 @@ readdirp@^2.0.0:
micromatch "^3.1.10"
readable-stream "^2.0.2"
realistic-structured-clone@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-2.0.2.tgz#2f8ec225b1f9af20efc79ac96a09043704414959"
integrity sha512-5IEvyfuMJ4tjQOuKKTFNvd+H9GSbE87IcendSBannE28PTrbolgaVg5DdEApRKhtze794iXqVUFKV60GLCNKEg==
dependencies:
core-js "^2.5.3"
domexception "^1.0.1"
typeson "^5.8.2"
typeson-registry "^1.0.0-alpha.20"
realpath-native@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.2.tgz#cd51ce089b513b45cf9b1516c82989b51ccc6560"
@ -16414,7 +16425,7 @@ tough-cookie@>=2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.4.3, tough-cookie@~2.
psl "^1.1.24"
punycode "^1.4.1"
tr46@^1.0.1:
tr46@^1.0.0, tr46@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
@ -16539,6 +16550,21 @@ typeforce@^1.11.3, typeforce@^1.11.5:
resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.15.1.tgz#fe24533cb3dbff2b79a3556bb3a3d4854850b2a5"
integrity sha512-RYgFG+2GXx4V7guHOhW0cvryg/iWAoopbF/WlOI25YoV9rsOQ0E8bKfYAEZbJL0LJ8yVqcwp77tDG+oebBSNNw==
typeson-registry@^1.0.0-alpha.20:
version "1.0.0-alpha.21"
resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.21.tgz#8a4e31abb471d482aa0306ad56d35af7633a166a"
integrity sha512-D7aj2KAzOaLvzCAYNhoFW536uWwbmSz5gSaO5fWETERLFheCOEIiMifGI4GDyGatxRvUJMz0ydp/ZEYYzs9iwQ==
dependencies:
base64-arraybuffer-es6 "0.3.1"
typeson "5.8.2"
uuid "3.2.1"
whatwg-url "6.4.0"
typeson@5.8.2, typeson@^5.8.2:
version "5.8.2"
resolved "https://registry.yarnpkg.com/typeson/-/typeson-5.8.2.tgz#cc26f45b705760a8777fba5d3c910cc3f0e8d7dd"
integrity sha512-AFuyvVdHdkGlVIalOrSFjylmeLeWFtKu77uDjEilP4B9+Jk0DCM1m1A4Q2s3AwROhgiFFVPI8oARaD9S61lOkg==
ua-parser-js@^0.7.18:
version "0.7.18"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
@ -16946,6 +16972,11 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==
uuid@^3.0.1, uuid@^3.2.1, uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
@ -17143,7 +17174,7 @@ webdriverio@^4.12.0:
wdio-dot-reporter "~0.0.8"
wgxpath "~1.0.0"
webidl-conversions@^4.0.2:
webidl-conversions@^4.0.1, webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
@ -17363,6 +17394,15 @@ whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171"
integrity sha512-5YSO1nMd5D1hY3WzAQV3PzZL83W3YeyR1yW9PcH26Weh1t+Vzh9B6XkDh7aXm83HBZ4nSMvkjvN2H2ySWIvBgw==
whatwg-url@6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.0.tgz#08fdf2b9e872783a7a1f6216260a1d66cc722e08"
integrity sha512-Z0CVh/YE217Foyb488eo+iBv+r7eAQ0wSTyApi9n06jhcA3z6Nidg/EGvl0UFkg7kMdKxfBzzr+o9JF+cevgMg==
dependencies:
lodash.sortby "^4.7.0"
tr46 "^1.0.0"
webidl-conversions "^4.0.1"
whatwg-url@^6.4.1:
version "6.5.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"

Loading…
Cancel
Save