diff --git a/app/containers/Root.js b/app/containers/Root.js index 20306011..6eb0262c 100644 --- a/app/containers/Root.js +++ b/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 ( - + @@ -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( diff --git a/app/index.js b/app/index.js index f49fc83b..3577cce1 100644 --- a/app/index.js +++ b/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() } } diff --git a/app/lib/i18n/index.js b/app/lib/i18n/index.js index 4e53e2c6..589a22ac 100644 --- a/app/lib/i18n/index.js +++ b/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') diff --git a/app/lib/lnd/config.js b/app/lib/lnd/config.js index dbf25a76..8529e6b9 100644 --- a/app/lib/lnd/config.js +++ b/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, - currency: $Keys, - network: $Keys, - wallet: string + chain: $Keys, + network: $Keys |} // Type definition for LndConfig constructor options. @@ -83,24 +82,17 @@ const safeUntildify = (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 diff --git a/app/lib/zap/controller.js b/app/lib/zap/controller.js index b85c402b..fa1ff530 100644 --- a/app/lib/zap/controller.js +++ b/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, + chain?: $Keys, + network?: $Keys, 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. diff --git a/app/lib/zap/menuBuilder.js b/app/lib/zap/menuBuilder.js index 0bee78a5..c0867e78 100644 --- a/app/lib/zap/menuBuilder.js +++ b/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)) } diff --git a/app/main.dev.js b/app/main.dev.js index 1bcc2258..73338df2 100644 --- a/app/main.dev.js +++ b/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. diff --git a/app/reducers/address.js b/app/reducers/address.js index 209a708f..29288f68 100644 --- a/app/reducers/address.js +++ b/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 }) diff --git a/app/reducers/channels.js b/app/reducers/channels.js index 0bfd4980..4216de7f 100644 --- a/app/reducers/channels.js +++ b/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' } }) } diff --git a/app/reducers/info.js b/app/reducers/info.js index 042ccdba..1b6aec2b 100644 --- a/app/reducers/info.js +++ b/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)) } diff --git a/app/reducers/invoice.js b/app/reducers/invoice.js index 536637bd..64bf5387 100644 --- a/app/reducers/invoice.js +++ b/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' } }) } diff --git a/app/reducers/lnd.js b/app/reducers/lnd.js index 9e7dfd6e..0a0d287b 100644 --- a/app/reducers/lnd.js +++ b/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) diff --git a/app/reducers/loading.js b/app/reducers/loading.js index c614642d..88a709c4 100644 --- a/app/reducers/loading.js +++ b/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 } diff --git a/app/reducers/locale.js b/app/reducers/locale.js index 1538259d..df808e39 100644 --- a/app/reducers/locale.js +++ b/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 // ------------------------------------ diff --git a/app/reducers/onboarding.js b/app/reducers/onboarding.js index 339060dd..ca489744 100644 --- a/app/reducers/onboarding.js +++ b/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 }) } diff --git a/app/reducers/theme.js b/app/reducers/theme.js index de4ab2f5..cd05841e 100644 --- a/app/reducers/theme.js +++ b/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 } } diff --git a/app/reducers/ticker.js b/app/reducers/ticker.js index 5aefd889..bb877fa4 100644 --- a/app/reducers/ticker.js +++ b/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: [ { diff --git a/app/store/db.js b/app/store/db.js new file mode 100644 index 00000000..3a5a36c6 --- /dev/null +++ b/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 diff --git a/app/themes/index.js b/app/themes/index.js index 317c072d..6f07d482 100644 --- a/app/themes/index.js +++ b/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 } diff --git a/package.json b/package.json index 46bc29f4..6384d034 100644 --- a/package.json +++ b/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", diff --git a/test/unit/__mocks__/dexie.js b/test/unit/__mocks__/dexie.js new file mode 100644 index 00000000..7ac91000 --- /dev/null +++ b/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 diff --git a/test/unit/lnd/lightning.spec.js b/test/unit/lnd/lightning.spec.js index 10ade8eb..5f331551 100644 --- a/test/unit/lnd/lightning.spec.js +++ b/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') diff --git a/test/unit/lnd/lnd-config.spec.js b/test/unit/lnd/lnd-config.spec.js index 2754f459..bc91ecac 100644 --- a/test/unit/lnd/lnd-config.spec.js +++ b/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' }) - }) }) }) }) diff --git a/test/unit/lnd/neutrino.spec.js b/test/unit/lnd/neutrino.spec.js index f9450002..715fa626 100644 --- a/test/unit/lnd/neutrino.spec.js +++ b/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 { diff --git a/test/unit/reducers/ticker.spec.js b/test/unit/reducers/ticker.spec.js index 93478523..afae26a5 100644 --- a/test/unit/reducers/ticker.spec.js +++ b/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', () => { diff --git a/test/unit/zap/controller.spec.js b/test/unit/zap/controller.spec.js index 7c4d36cf..5e9de8a9 100644 --- a/test/unit/zap/controller.spec.js +++ b/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() }) diff --git a/yarn.lock b/yarn.lock index af4267ab..5f0362ea 100644 --- a/yarn.lock +++ b/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"