diff --git a/app/components/App/App.js b/app/components/App/App.js index 1270a238..a52b5f08 100644 --- a/app/components/App/App.js +++ b/app/components/App/App.js @@ -25,14 +25,20 @@ class App extends React.Component { activityModalProps: PropTypes.object, receiveModalProps: PropTypes.object, channelFormProps: PropTypes.object, + setIsWalletOpen: PropTypes.func.isRequired, fetchInfo: PropTypes.func.isRequired, fetchDescribeNetwork: PropTypes.func.isRequired } componentDidMount() { - const { fetchInfo, fetchDescribeNetwork } = this.props + const { fetchInfo, fetchDescribeNetwork, setIsWalletOpen } = this.props + + // Set wallet open state. + setIsWalletOpen(true) + // fetch node info. fetchInfo() + // fetch LN network from nodes POV. fetchDescribeNetwork() } diff --git a/app/components/Home/CreateWalletButton.js b/app/components/Home/CreateWalletButton.js new file mode 100644 index 00000000..76e07e2c --- /dev/null +++ b/app/components/Home/CreateWalletButton.js @@ -0,0 +1,18 @@ +import React from 'react' +import { Flex } from 'rebass' +import { Button, Text } from 'components/UI' +import PlusCircle from 'components/Icon/PlusCircle' + +const CreateWalletButton = ({ ...rest }) => ( + +) +export default CreateWalletButton diff --git a/app/components/Home/Home.js b/app/components/Home/Home.js new file mode 100644 index 00000000..d946d6a8 --- /dev/null +++ b/app/components/Home/Home.js @@ -0,0 +1,126 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Route, Switch, withRouter } from 'react-router-dom' +import { Box } from 'rebass' +import { Bar, Heading, MainContent, Sidebar } from 'components/UI' +import ZapLogo from 'components/Icon/ZapLogo' +import { CreateWalletButton, WalletLauncher, WalletsMenu, WalletUnlocker } from '.' + +const NoMatch = () => ( + + Please select a wallet + +) + +class Home extends React.Component { + static propTypes = { + activeWallet: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + deleteWallet: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, + lightningGrpcActive: PropTypes.bool.isRequired, + walletUnlockerGrpcActive: PropTypes.bool.isRequired, + wallets: PropTypes.array.isRequired, + startLnd: PropTypes.func.isRequired, + stopLnd: PropTypes.func.isRequired, + setActiveWallet: PropTypes.func.isRequired, + unlockWallet: PropTypes.func.isRequired, + setUnlockWalletError: PropTypes.func.isRequired, + unlockingWallet: PropTypes.bool, + unlockWalletError: PropTypes.string + } + + /** + * Handle click event on the Create new wallet button, + */ + handleCreateNewWalletClick = () => { + const { history } = this.props + history.push('/onboarding') + } + + render() { + const { + activeWallet, + deleteWallet, + startLnd, + unlockWallet, + wallets, + setActiveWallet, + stopLnd, + lightningGrpcActive, + walletUnlockerGrpcActive, + setUnlockWalletError, + unlockingWallet, + unlockWalletError + } = this.props + + return ( + <> + + + + + + + + + + + + + + + { + const wallet = wallets.find(wallet => wallet.id == params.walletId) + if (!wallet) { + return null + } + return ( + + ) + }} + /> + { + const wallet = wallets.find(wallet => wallet.id == params.walletId) + if (!wallet) { + return null + } + return ( + + ) + }} + /> + + + + + + ) + } +} + +export default withRouter(Home) diff --git a/app/components/Home/WalletHeader.js b/app/components/Home/WalletHeader.js new file mode 100644 index 00000000..b7bcfb49 --- /dev/null +++ b/app/components/Home/WalletHeader.js @@ -0,0 +1,18 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Box } from 'rebass' +import { Heading, Truncate } from 'components/UI' + +const WalletHeader = ({ title }) => ( + + + + + +) + +WalletHeader.propTypes = { + title: PropTypes.string.isRequired +} + +export default WalletHeader diff --git a/app/components/Home/WalletLauncher.js b/app/components/Home/WalletLauncher.js new file mode 100644 index 00000000..49723afd --- /dev/null +++ b/app/components/Home/WalletLauncher.js @@ -0,0 +1,117 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { withRouter } from 'react-router-dom' +import { Box, Flex } from 'rebass' +import { Bar, Button, Heading } from 'components/UI' +import ArrowRight from 'components/Icon/ArrowRight' +import { WalletSettingsFormLocal, WalletSettingsFormRemote, WalletHeader } from '.' + +class WalletLauncher extends React.Component { + static propTypes = { + wallet: PropTypes.object.isRequired, + deleteWallet: PropTypes.func.isRequired, + startLnd: PropTypes.func.isRequired, + lightningGrpcActive: PropTypes.bool.isRequired, + walletUnlockerGrpcActive: PropTypes.bool.isRequired, + stopLnd: PropTypes.func.isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired + }) + } + + componentDidMount() { + const { stopLnd } = this.props + stopLnd() + } + + /** + * Redirect to the login page when we establish a connection to lnd. + */ + componentDidUpdate(prevProps) { + const { history, lightningGrpcActive, walletUnlockerGrpcActive, wallet } = this.props + + // If the wallet unlocker became active, switch to the login screen + if (walletUnlockerGrpcActive && !prevProps.walletUnlockerGrpcActive) { + history.push(`/home/wallet/${wallet.id}/unlock`) + } + + // If an active wallet connection has been established, switch to the app. + if (lightningGrpcActive && !prevProps.lightningGrpcActive) { + if (wallet.type === 'local') { + history.push('/syncing') + } else { + history.push('/app') + } + } + } + + walletName = wallet => { + if (wallet.type === 'local') { + return wallet.alias || `Wallet #${wallet.id}` + } + return wallet.host.split(':')[0] + } + + handleDelete = () => { + const { deleteWallet, wallet } = this.props + deleteWallet(wallet.id) + } + + render() { + const { startLnd, wallet } = this.props + const walletName = this.walletName(wallet) + + return ( + + + + + + + + + + + {wallet.type === 'local' && ( + <> + Settings + + + + )} + + {wallet.type !== 'local' && ( + <> + + + )} + + ) + } +} + +export default withRouter(WalletLauncher) diff --git a/app/components/Home/WalletSettingsFormLocal.js b/app/components/Home/WalletSettingsFormLocal.js new file mode 100644 index 00000000..1bbc8f64 --- /dev/null +++ b/app/components/Home/WalletSettingsFormLocal.js @@ -0,0 +1,221 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Flex } from 'rebass' +import { DataRow, Form, Input, Label, Range, Toggle } from 'components/UI' +import * as yup from 'yup' + +class WalletSettingsFormLocal extends React.Component { + static propTypes = { + wallet: PropTypes.object.isRequired, + startLnd: PropTypes.func.isRequired + } + + validateAutopilot = value => { + try { + yup.boolean().validateSync(value) + } catch (error) { + return error.message + } + } + + validateAutopilotAllocation = value => { + try { + yup + .number() + .required() + .positive() + .min(0) + .max(100) + .typeError('A number is required') + .validateSync(value) + } catch (error) { + return error.message + } + } + + validateAutopilotMaxchannels = value => { + try { + yup + .number() + .required() + .positive() + .integer() + .max(100) + .typeError('A number is required') + .validateSync(value) + } catch (error) { + return error.message + } + } + + validateAutopilotChansize = value => { + try { + yup + .number() + .required() + .positive() + .integer() + .max(100000000) + .typeError('A number is required') + .validateSync(value) + } catch (error) { + return error.message + } + } + + preSubmit = values => { + if (values.autopilotAllocation) { + values.autopilotAllocation = values.autopilotAllocation / 100 + } + return values + } + + onSubmit = async values => { + const { startLnd } = this.props + return startLnd(values) + } + + setFormApi = formApi => { + this.formApi = formApi + } + + render() { + const { wallet, startLnd, ...rest } = this.props + + return ( +
+ {({ formState }) => ( + + Alias} + right={} + /> + + Autopilot} + right={ + + } + /> + + {formState.values.autopilot ? ( + + Percentage of Balance} + right={ + + + + + } + /> + + Number of Channels max} + right={ + + } + /> + + Minimum channel size} + right={ + + } + /> + + Maximum channel size} + right={ + + } + /> + + ) : null} + + )} +
+ ) + } +} + +export default WalletSettingsFormLocal diff --git a/app/components/Home/WalletSettingsFormRemote.js b/app/components/Home/WalletSettingsFormRemote.js new file mode 100644 index 00000000..1bde799d --- /dev/null +++ b/app/components/Home/WalletSettingsFormRemote.js @@ -0,0 +1,39 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Card } from 'rebass' +import { Form } from 'components/UI' + +class WalletSettingsFormRemote extends React.Component { + static propTypes = { + wallet: PropTypes.object.isRequired, + startLnd: PropTypes.func.isRequired + } + + onSubmit = async values => { + const { startLnd } = this.props + return startLnd(values) + } + + setFormApi = formApi => { + this.formApi = formApi + } + + render() { + const { wallet, startLnd, ...rest } = this.props + return ( +
+ +
{JSON.stringify(wallet, null, 2)}
+
+
+ ) + } +} + +export default WalletSettingsFormRemote diff --git a/app/components/Home/WalletUnlocker.js b/app/components/Home/WalletUnlocker.js new file mode 100644 index 00000000..650bb1df --- /dev/null +++ b/app/components/Home/WalletUnlocker.js @@ -0,0 +1,130 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { withRouter } from 'react-router-dom' +import { Form } from 'informed' +import { Button, PasswordInput } from 'components/UI' +import * as yup from 'yup' +import { WalletHeader } from '.' + +/** + * @render react + * @name WalletUnlocker + * @example + * {}} + setError={() => {}} > + */ +class WalletUnlocker extends React.Component { + static displayName = 'WalletUnlocker' + + static propTypes = { + wallet: PropTypes.object.isRequired, + lightningGrpcActive: PropTypes.bool.isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired + }), + unlockingWallet: PropTypes.bool, + unlockWalletError: PropTypes.string, + unlockWallet: PropTypes.func.isRequired, + setUnlockWalletError: PropTypes.func.isRequired + } + + componentDidUpdate(prevProps) { + const { + wallet, + lightningGrpcActive, + history, + setUnlockWalletError, + unlockingWallet, + unlockWalletError + } = this.props + + // Set the form error if we got an error unlocking. + if (unlockWalletError && !prevProps.unlockWalletError) { + this.formApi.setError('password', unlockWalletError) + setUnlockWalletError(null) + } + + // Redirect to the app if the wallet was successfully unlocked. + if (!unlockingWallet && prevProps.unlockingWallet && !unlockWalletError) { + if (wallet.type === 'local') { + history.push('/syncing') + } else { + history.push('/app') + } + } + + // If an active wallet connection has been established, switch to the app. + if (lightningGrpcActive && !prevProps.lightningGrpcActive) { + if (wallet.type === 'local') { + history.push('/syncing') + } else { + history.push('/app') + } + } + } + + setFormApi = formApi => { + this.formApi = formApi + } + + onSubmit = values => { + const { unlockWallet } = this.props + unlockWallet(values.password) + } + + walletName = wallet => { + if (wallet.type === 'local') { + return wallet.alias || `Wallet #${wallet.id}` + } + return wallet.host.split(':')[0] + } + + validatePassword = value => { + try { + yup + .string() + .required() + .min(8) + .validateSync(value) + } catch (error) { + return error.message + } + } + + render = () => { + const { wallet } = this.props + const walletName = this.walletName(wallet) + + return ( +
+ {({ formState }) => ( + + + + + + + + )} +
+ ) + } +} + +export default withRouter(WalletUnlocker) diff --git a/app/components/Home/WalletsMenu.js b/app/components/Home/WalletsMenu.js new file mode 100644 index 00000000..1dfc9ab1 --- /dev/null +++ b/app/components/Home/WalletsMenu.js @@ -0,0 +1,60 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { NavLink } from 'react-router-dom' +import { Box } from 'rebass' +import { Text } from 'components/UI' + +const walletName = wallet => { + if (wallet.type === 'local') { + return wallet.alias || `Wallet #${wallet.id}` + } + return wallet.host.split(':')[0] +} + +const WalletGroup = ({ setActiveWallet, title, wallets, ...rest }) => ( + + {title} + {wallets.map(wallet => ( + + setActiveWallet(wallet.id)} + > + {walletName(wallet)} + + + ))} + +) + +class WalletsMenu extends React.Component { + static displayName = 'WalletsMenu' + + static propTypes = { + setActiveWallet: PropTypes.func.isRequired, + wallets: PropTypes.array.isRequired + } + + render() { + const { setActiveWallet, wallets, ...rest } = this.props + const localWallets = wallets.filter(wallet => wallet.type === 'local') + const otherWallets = wallets.filter(wallet => wallet.type !== 'local') + + return ( + + + {otherWallets.length > 0 && ( + + )} + + ) + } +} + +export default WalletsMenu diff --git a/app/components/Home/index.js b/app/components/Home/index.js new file mode 100644 index 00000000..cda94636 --- /dev/null +++ b/app/components/Home/index.js @@ -0,0 +1,8 @@ +export Home from './Home' +export CreateWalletButton from './CreateWalletButton' +export WalletsMenu from './WalletsMenu' +export WalletLauncher from './WalletLauncher' +export WalletUnlocker from './WalletUnlocker' +export WalletSettingsFormLocal from './WalletSettingsFormLocal' +export WalletSettingsFormRemote from './WalletSettingsFormRemote' +export WalletHeader from './WalletHeader' diff --git a/app/components/Onboarding/Onboarding.js b/app/components/Onboarding/Onboarding.js index e3a5c54e..2808ac69 100644 --- a/app/components/Onboarding/Onboarding.js +++ b/app/components/Onboarding/Onboarding.js @@ -31,8 +31,8 @@ class Onboarding extends React.Component { connectionCert: PropTypes.string, connectionMacaroon: PropTypes.string, connectionString: PropTypes.string, - lndWalletStarted: PropTypes.bool, - lndWalletUnlockerStarted: PropTypes.bool, + lightningGrpcActive: PropTypes.bool, + walletUnlockerGrpcActive: PropTypes.bool, seed: PropTypes.array, startLndHostError: PropTypes.string, startLndCertError: PropTypes.string, @@ -41,7 +41,7 @@ class Onboarding extends React.Component { fetchingSeed: PropTypes.bool, // DISPATCH createNewWallet: PropTypes.func.isRequired, - generateSeed: PropTypes.func.isRequired, + fetchSeed: PropTypes.func.isRequired, recoverOldWallet: PropTypes.func.isRequired, resetOnboarding: PropTypes.func.isRequired, setAlias: PropTypes.func.isRequired, @@ -75,8 +75,8 @@ class Onboarding extends React.Component { connectionCert, connectionMacaroon, connectionString, - lndWalletStarted, - lndWalletUnlockerStarted, + lightningGrpcActive, + walletUnlockerGrpcActive, seed, startLndHostError, startLndCertError, @@ -97,7 +97,7 @@ class Onboarding extends React.Component { validateHost, validateCert, validateMacaroon, - generateSeed, + fetchSeed, resetOnboarding, createNewWallet, recoverOldWallet, @@ -116,7 +116,7 @@ class Onboarding extends React.Component { , , , @@ -171,8 +171,8 @@ class Onboarding extends React.Component { connectionHost, connectionCert, connectionMacaroon, - lndWalletStarted, - lndWalletUnlockerStarted, + lightningGrpcActive, + walletUnlockerGrpcActive, startLndHostError, startLndCertError, startLndMacaroonError, @@ -207,8 +207,8 @@ class Onboarding extends React.Component { {...{ connectionType, connectionString, - lndWalletStarted, - lndWalletUnlockerStarted, + lightningGrpcActive, + walletUnlockerGrpcActive, startLndHostError, startLndCertError, startLndMacaroonError, diff --git a/app/components/Onboarding/Steps/ConnectionConfirm.js b/app/components/Onboarding/Steps/ConnectionConfirm.js index 0bf1ef78..2ed940ce 100644 --- a/app/components/Onboarding/Steps/ConnectionConfirm.js +++ b/app/components/Onboarding/Steps/ConnectionConfirm.js @@ -34,8 +34,8 @@ class ConnectionConfirm extends React.Component { startLndCertError: PropTypes.string, startLndMacaroonError: PropTypes.string, startLnd: PropTypes.func.isRequired, - lndWalletUnlockerStarted: PropTypes.bool, - lndWalletStarted: PropTypes.bool + walletUnlockerGrpcActive: PropTypes.bool, + lightningGrpcActive: PropTypes.bool } static defaultProps = { @@ -81,8 +81,8 @@ class ConnectionConfirm extends React.Component { connectionCert, connectionMacaroon, connectionString, - lndWalletStarted, - lndWalletUnlockerStarted, + lightningGrpcActive, + walletUnlockerGrpcActive, startLndHostError, startLndCertError, startLndMacaroonError, diff --git a/app/components/Onboarding/Steps/Login.js b/app/components/Onboarding/Steps/Login.js index 92cd6bdc..027543bd 100644 --- a/app/components/Onboarding/Steps/Login.js +++ b/app/components/Onboarding/Steps/Login.js @@ -9,7 +9,6 @@ class Login extends React.Component { static propTypes = { wizardApi: PropTypes.object, wizardState: PropTypes.object, - walletDir: PropTypes.string.isRequired, unlockWalletError: PropTypes.string, setUnlockWalletError: PropTypes.func.isRequired, unlockWallet: PropTypes.func.isRequired @@ -43,7 +42,6 @@ class Login extends React.Component { const { wizardApi, wizardState, - walletDir, unlockWallet, unlockWalletError, setUnlockWalletError, diff --git a/app/components/Onboarding/Steps/SeedConfirm.js b/app/components/Onboarding/Steps/SeedConfirm.js index bf1ee3f7..31356736 100644 --- a/app/components/Onboarding/Steps/SeedConfirm.js +++ b/app/components/Onboarding/Steps/SeedConfirm.js @@ -22,10 +22,10 @@ class SeedConfirm extends React.Component { } componentDidMount() { - this.generateSeedWordIndexes() + this.fetchSeedWordIndexes() } - generateSeedWordIndexes = () => { + fetchSeedWordIndexes = () => { const seedWordIndexes = [] while (seedWordIndexes.length < 3) { const r = Math.floor(Math.random() * 24) + 1 diff --git a/app/components/Onboarding/Steps/SeedView.js b/app/components/Onboarding/Steps/SeedView.js index fdcd5352..63827523 100644 --- a/app/components/Onboarding/Steps/SeedView.js +++ b/app/components/Onboarding/Steps/SeedView.js @@ -27,7 +27,7 @@ class SeedView extends React.Component { wizardState: PropTypes.object, seed: PropTypes.array, fetchingSeed: PropTypes.bool, - generateSeed: PropTypes.func.isRequired + fetchSeed: PropTypes.func.isRequired } static defaultProps = { @@ -38,14 +38,14 @@ class SeedView extends React.Component { } async componentDidMount() { - const { seed, generateSeed } = this.props + const { seed, fetchSeed } = this.props if (seed.length === 0) { - generateSeed() + fetchSeed() } } render() { - const { wizardApi, wizardState, seed, generateSeed, fetchingSeed, intl, ...rest } = this.props + const { wizardApi, wizardState, seed, fetchSeed, fetchingSeed, intl, ...rest } = this.props const { getApi, preSubmit, onSubmit, onSubmitFailure } = wizardApi return ( diff --git a/app/components/Onboarding/Steps/messages.js b/app/components/Onboarding/Steps/messages.js index 7ea1865f..62ca62a7 100644 --- a/app/components/Onboarding/Steps/messages.js +++ b/app/components/Onboarding/Steps/messages.js @@ -41,8 +41,7 @@ export default defineMessages({ hostname_title: 'Host', import_description: "Recovering a wallet, nice. You don't need anyone else, you got yourself :)", import_title: 'Import your seed', - login_description: - 'It looks like you already have a wallet (wallet found at: `{walletDir}`). Please enter your wallet password to unlock it.', + login_description: 'Please enter your wallet password to unlock it.', login_title: 'Welcome back!', macaroon_description: 'Path to the lnd macaroon file. Example: /path/to/admin.macaroon', next: 'Next', diff --git a/app/containers/App.js b/app/containers/App.js index 0604478a..d0e6481f 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -42,7 +42,7 @@ import { fetchBalance } from 'reducers/balance' import { fetchDescribeNetwork } from 'reducers/network' import { clearError } from 'reducers/error' import { hideActivityModal, activitySelectors } from 'reducers/activity' - +import { setIsWalletOpen } from 'reducers/wallet' import App from 'components/App' import withLoading from 'components/withLoading' @@ -77,7 +77,8 @@ const mapDispatchToProps = { updateManualFormErrors, setChannelFormType, fetchDescribeNetwork, - hideActivityModal + hideActivityModal, + setIsWalletOpen } const mapStateToProps = state => ({ @@ -98,6 +99,7 @@ const mapStateToProps = state => ({ error: state.error, network: state.network, settings: state.settings, + wallet: state.wallet, isLoading: infoSelectors.infoLoading(state) || diff --git a/app/containers/Home.js b/app/containers/Home.js new file mode 100644 index 00000000..4882ccc5 --- /dev/null +++ b/app/containers/Home.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux' +import { setActiveWallet, walletSelectors, deleteWallet } from 'reducers/wallet' +import { setUnlockWalletError, stopLnd, startLnd, unlockWallet } from 'reducers/lnd' +import { Home } from 'components/Home' + +const mapStateToProps = state => ({ + wallets: state.wallet.wallets, + activeWallet: walletSelectors.activeWallet(state), + activeWalletSettings: walletSelectors.activeWalletSettings(state), + lightningGrpcActive: state.lnd.lightningGrpcActive, + walletUnlockerGrpcActive: state.lnd.walletUnlockerGrpcActive, + unlockingWallet: state.lnd.unlockingWallet, + unlockWalletError: state.lnd.unlockWalletError +}) + +const mapDispatchToProps = { + setActiveWallet, + setUnlockWalletError, + stopLnd, + startLnd, + unlockWallet, + deleteWallet +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Home) diff --git a/app/containers/Initializer.js b/app/containers/Initializer.js new file mode 100644 index 00000000..f7473c0a --- /dev/null +++ b/app/containers/Initializer.js @@ -0,0 +1,92 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { withRouter } from 'react-router' +import { walletSelectors } from 'reducers/wallet' +import { startActiveWallet } from 'reducers/lnd' + +/** + * Root component that deals with mounting the app and managing top level routing. + */ +class Initializer extends React.Component { + static propTypes = { + onboarding: PropTypes.bool, + history: PropTypes.object.isRequired, + activeWallet: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + activeWalletSettings: PropTypes.object, + isWalletOpen: PropTypes.bool, + lightningGrpcActive: PropTypes.bool, + walletUnlockerGrpcActive: PropTypes.bool, + startActiveWallet: PropTypes.func.isRequired + } + + /** + * Redirect to the correct page when we establish a connection to lnd. + */ + componentDidUpdate(prevProps) { + const { + onboarding, + history, + activeWallet, + activeWalletSettings, + isWalletOpen, + lightningGrpcActive, + walletUnlockerGrpcActive, + startActiveWallet + } = this.props + + // Wait unti we are onboarding before doing anything. + if (!onboarding) { + return + } + + // If we have just determined that the user has an active wallet, attempt to start it. + if (typeof activeWallet !== 'undefined') { + if (activeWalletSettings) { + if (isWalletOpen) { + startActiveWallet() + } else { + history.push(`/home/wallet/${activeWallet}`) + } + } else { + history.push('/onboarding') + } + } + + // If the wallet unlocker became active, switch to the login screen + if (walletUnlockerGrpcActive && !prevProps.walletUnlockerGrpcActive) { + history.push(`/home/wallet/${activeWallet}/unlock`) + } + + // If an active wallet connection has been established, switch to the app. + if (lightningGrpcActive && !prevProps.lightningGrpcActive) { + if (activeWalletSettings.type === 'local') { + history.push('/syncing') + } else { + history.push('/app') + } + } + } + + render() { + return null + } +} + +const mapStateToProps = state => ({ + onboarding: state.onboarding.onboarding, + activeWallet: walletSelectors.activeWallet(state), + activeWalletSettings: walletSelectors.activeWalletSettings(state), + lightningGrpcActive: state.lnd.lightningGrpcActive, + walletUnlockerGrpcActive: state.lnd.walletUnlockerGrpcActive, + isWalletOpen: state.wallet.isWalletOpen +}) + +const mapDispatchToProps = { + startActiveWallet +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(Initializer)) diff --git a/app/containers/Logout.js b/app/containers/Logout.js new file mode 100644 index 00000000..74d6a9eb --- /dev/null +++ b/app/containers/Logout.js @@ -0,0 +1,44 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { withRouter } from 'react-router' +import { restart } from 'reducers/lnd' +import { setIsWalletOpen } from 'reducers/wallet' + +/** + * Root component that deals with mounting the app and managing top level routing. + */ +class Logout extends React.Component { + static propTypes = { + lightningGrpcActive: PropTypes.bool, + walletUnlockerGrpcActive: PropTypes.bool, + restart: PropTypes.func.isRequired + } + + componentDidMount() { + const { lightningGrpcActive, walletUnlockerGrpcActive, restart } = this.props + if (lightningGrpcActive || walletUnlockerGrpcActive) { + setIsWalletOpen(false) + restart() + } + } + + render() { + return null + } +} + +const mapStateToProps = state => ({ + lightningGrpcActive: state.lnd.lightningGrpcActive, + walletUnlockerGrpcActive: state.lnd.walletUnlockerGrpcActive +}) + +const mapDispatchToProps = { + restart, + setIsWalletOpen +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(Logout)) diff --git a/app/containers/Onboarding.js b/app/containers/Onboarding.js index 9fa115e6..ffec7b7e 100644 --- a/app/containers/Onboarding.js +++ b/app/containers/Onboarding.js @@ -9,18 +9,20 @@ import { setConnectionMacaroon, setConnectionString, setPassword, - setUnlockWalletError, - startLnd, - stopLnd, validateHost, validateCert, validateMacaroon, - generateSeed, + resetOnboarding +} from 'reducers/onboarding' +import { + setUnlockWalletError, + startLnd, + stopLnd, + fetchSeed, createNewWallet, recoverOldWallet, - resetOnboarding, unlockWallet -} from 'reducers/onboarding' +} from 'reducers/lnd' const mapStateToProps = state => ({ alias: state.onboarding.alias, @@ -30,16 +32,15 @@ const mapStateToProps = state => ({ connectionCert: state.onboarding.connectionCert, connectionMacaroon: state.onboarding.connectionMacaroon, connectionString: state.onboarding.connectionString, - lndWalletStarted: state.onboarding.lndWalletStarted, - lndWalletUnlockerStarted: state.onboarding.lndWalletUnlockerStarted, - startLndHostError: state.onboarding.startLndHostError, - startLndCertError: state.onboarding.startLndCertError, - startLndMacaroonError: state.onboarding.startLndMacaroonError, + lightningGrpcActive: state.lnd.lightningGrpcActive, + walletUnlockerGrpcActive: state.lnd.walletUnlockerGrpcActive, + startLndHostError: state.lnd.startLndHostError, + startLndCertError: state.lnd.startLndCertError, + startLndMacaroonError: state.lnd.startLndMacaroonError, seed: state.onboarding.seed, - signupMode: state.onboarding.signupMode, - unlockWalletError: state.onboarding.unlockWalletError, + unlockWalletError: state.lnd.unlockWalletError, onboarded: state.onboarding.onboarded, - fetchingSeed: state.onboarding.fetchingSeed + fetchingSeed: state.lnd.fetchingSeed }) const mapDispatchToProps = { @@ -57,7 +58,7 @@ const mapDispatchToProps = { validateHost, validateCert, validateMacaroon, - generateSeed, + fetchSeed, createNewWallet, recoverOldWallet, resetOnboarding, diff --git a/app/containers/Root.js b/app/containers/Root.js index 34d8c2f3..6c56319a 100644 --- a/app/containers/Root.js +++ b/app/containers/Root.js @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { Redirect, Route, Switch } from 'react-router-dom' +import { Route, Switch } from 'react-router-dom' import { ConnectedRouter } from 'connected-react-router' import { ThemeProvider } from 'styled-components' @@ -9,12 +9,16 @@ import { clearError, errorSelectors } from 'reducers/error' import { loadingSelectors, setLoading, setMounted } from 'reducers/loading' import { initCurrency, initLocale } from 'reducers/locale' import { initTheme, themeSelectors } from 'reducers/theme' +import { initWallets, walletSelectors } from 'reducers/wallet' import { fetchTicker, tickerSelectors } from 'reducers/ticker' import { fetchSuggestedNodes } from 'reducers/channels' -import { Page, Titlebar, GlobalStyle } from 'components/UI' +import { Page, Titlebar, GlobalStyle, Modal } from 'components/UI' import GlobalError from 'components/GlobalError' import withLoading from 'components/withLoading' +import Initializer from './Initializer' +import Logout from './Logout' +import Home from './Home' import Onboarding from './Onboarding' import Syncing from './Syncing' import App from './App' @@ -30,6 +34,7 @@ const SPLASH_SCREEN_TIME = 1500 */ class Root extends React.Component { static propTypes = { + hasWallets: PropTypes.bool, clearError: PropTypes.func.isRequired, currentTicker: PropTypes.object, theme: PropTypes.object, @@ -40,6 +45,7 @@ class Root extends React.Component { initLocale: PropTypes.func.isRequired, initCurrency: PropTypes.func.isRequired, initTheme: PropTypes.func.isRequired, + initWallets: PropTypes.func.isRequired, isLoading: PropTypes.bool.isRequired, isMounted: PropTypes.bool.isRequired, setLoading: PropTypes.func.isRequired, @@ -67,6 +73,7 @@ class Root extends React.Component { initLocale, initCurrency, initTheme, + initWallets, isLoading, isMounted, setLoading, @@ -80,6 +87,7 @@ class Root extends React.Component { initTheme() initLocale() initCurrency() + initWallets() fetchTicker() fetchSuggestedNodes() } @@ -100,9 +108,9 @@ class Root extends React.Component { } render() { - const { clearError, theme, error, history, isLoading } = this.props + const { hasWallets, clearError, theme, error, history, isLoading } = this.props - // Wait until we have loaded essential data before displaying anything.. + // Wait until we have loaded essential data before displaying anything. if (!theme) { return null } @@ -116,10 +124,28 @@ class Root extends React.Component { - } /> - - - + + + ( + history.push('/home')}> + + + )} + /> + ( + history.push('/logout')} pb={0} px={0}> + + + )} + /> + + @@ -130,6 +156,7 @@ class Root extends React.Component { } const mapStateToProps = state => ({ + hasWallets: walletSelectors.hasWallets(state), currentTicker: tickerSelectors.currentTicker(state), theme: themeSelectors.currentThemeSettings(state), error: errorSelectors.getErrorState(state), @@ -144,6 +171,7 @@ const mapDispatchToProps = { initCurrency, initLocale, initTheme, + initWallets, setLoading, setMounted } diff --git a/app/lib/lnd/config.js b/app/lib/lnd/config.js index 8529e6b9..c2b9b633 100644 --- a/app/lib/lnd/config.js +++ b/app/lib/lnd/config.js @@ -31,7 +31,13 @@ export const networks = { // Type definition for for local connection settings. type LndConfigSettingsLocalType = {| alias?: string, - autopilot?: boolean + autopilot?: boolean, + autopilotMaxchannels?: number, + autopilotAllocation?: number, + autopilotMinchansize?: number, + autopilotMaxchansize?: number, + autopilotPrivate?: boolean, + autopilotMinconfs?: number |} // Type definition for for custom connection settings. @@ -83,11 +89,30 @@ const safeUntildify = (val: ?T): ?T => (typeof val === 'string' ? untildify(v */ class LndConfig { static SETTINGS_PROPS = { - local: ['alias', 'autopilot'], + local: [ + 'alias', + 'autopilot', + 'autopilotMaxchannels', + 'autopilotAllocation', + 'autopilotMinchansize', + 'autopilotMaxchansize', + 'autopilotPrivate', + 'autopilotMinconfs' + ], custom: ['host', 'cert', 'macaroon'], btcpayserver: ['host', 'macaroon', 'string'] } + static SETTINGS_DEFAULTS = { + autopilot: true, + autopilotMaxchannels: 5, + autopilotMinchansize: 20000, + autopilotMaxchansize: 16777215, + autopilotAllocation: 0.6, + autopilotPrivate: true, + autopilotMinconfs: 0 + } + // Type descriptor properties. id: number type: string @@ -101,6 +126,12 @@ class LndConfig { string: ?string alias: ?string autopilot: ?boolean + autopilotMaxchannels: ?number + autopilotMinchansize: ?number + autopilotMaxchansize: ?number + autopilotAllocation: ?number + autopilotPrivate: ?boolean + autopilotMinconfs: ?number // Read only data properties. +wallet: string @@ -220,11 +251,30 @@ class LndConfig { this.chain = options.chain this.network = options.network - // If settings were provided then clean them up and assign them to the instance for easy access. - if (options.settings) { - debug('Setting settings as: %o', options.settings) - Object.assign(this, options.settings) - } + // Merge in other whitelisted settings. + let settings = Object.assign({}, LndConfig.SETTINGS_DEFAULTS, options.settings) + const filteredSettings = Object.keys(settings) + .filter(key => LndConfig.SETTINGS_PROPS[this.type].includes(key)) + .reduce((obj, key) => { + let value = settings[key] + if ( + [ + 'autopilotMaxchannels', + 'autopilotMinchansize', + 'autopilotMaxchansize', + 'autopilotAllocation', + 'autopilotMinconfs' + ].includes(key) + ) { + value = Number(settings[key]) + } + return { + ...obj, + [key]: value + } + }, {}) + debug('Setting settings as: %o', filteredSettings) + Object.assign(this, filteredSettings) } // For local configs host/cert/macaroon are auto-generated. diff --git a/app/lib/lnd/neutrino.js b/app/lib/lnd/neutrino.js index 59383c9b..34d09c94 100644 --- a/app/lib/lnd/neutrino.js +++ b/app/lib/lnd/neutrino.js @@ -91,14 +91,31 @@ class Neutrino extends EventEmitter { port: [9735, 9734, 9733, 9732, 9731, 9736, 9737, 9738, 9739] }) - //Configure lnd. + // Genreate autopilot config. + const autopilotArgMap: Object = { + autopilotAllocation: '--autopilot.allocation', + autopilotMaxchannels: '--autopilot.maxchannels', + autopilotMinchansize: '--autopilot.minchansize', + autopilotMaxchansize: '--autopilot.maxchansize', + autopilotMinconfs: '--autopilot.minconfs' + } + const autopilotConf = [] + Object.entries(this.lndConfig).forEach(([key, value]) => { + if (Object.keys(autopilotArgMap).includes(key)) { + autopilotConf.push(`${autopilotArgMap[key]}=${String(value)}`) + } + }) + + // Configure lnd. const neutrinoArgs = [ `--configfile=${this.lndConfig.configPath}`, `--lnddir=${this.lndConfig.lndDir}`, `--listen=0.0.0.0:${p2pListen}`, `--rpclisten=localhost:${rpcListen}`, + `${this.lndConfig.alias ? `--alias=${this.lndConfig.alias}` : ''}`, `${this.lndConfig.autopilot ? '--autopilot.active' : ''}`, - `${this.lndConfig.alias ? `--alias=${this.lndConfig.alias}` : ''}` + `${this.lndConfig.autopilotPrivate ? '--autopilot.private' : ''}`, + ...autopilotConf ] // Configure neutrino backend. diff --git a/app/lib/lnd/walletUnlockerMethods/index.js b/app/lib/lnd/walletUnlockerMethods/index.js index 913a79fa..d02147b7 100644 --- a/app/lib/lnd/walletUnlockerMethods/index.js +++ b/app/lib/lnd/walletUnlockerMethods/index.js @@ -18,8 +18,8 @@ export default function(walletUnlocker, log, event, msg, data, lndConfig) { case 'genSeed': walletController .genSeed(walletUnlocker) - .then(genSeedData => event.sender.send('receiveSeed', genSeedData)) - .catch(error => event.sender.send('receiveSeedError', decorateError(error))) + .then(genSeedData => event.sender.send('fetchSeedSuccess', genSeedData)) + .catch(error => event.sender.send('fetchSeedError', decorateError(error))) break case 'unlockWallet': walletController diff --git a/app/lib/zap/controller.js b/app/lib/zap/controller.js index 95058ee7..60e54a22 100644 --- a/app/lib/zap/controller.js +++ b/app/lib/zap/controller.js @@ -70,12 +70,16 @@ class ZapController { { name: 'startLocalLnd', from: 'onboarding', to: 'running' }, { name: 'startRemoteLnd', from: 'onboarding', to: 'connected' }, { name: 'stopLnd', from: '*', to: 'onboarding' }, + { name: 'restart', from: '*', to: 'onboarding' }, { name: 'terminate', from: '*', to: 'terminated' } ], methods: { onOnboarding: this.onOnboarding.bind(this), + onStartOnboarding: this.onStartOnboarding.bind(this), onBeforeStartLocalLnd: this.onBeforeStartLocalLnd.bind(this), onBeforeStartRemoteLnd: this.onBeforeStartRemoteLnd.bind(this), + onBeforeStopLnd: this.onBeforeStopLnd.bind(this), + onBeforeRestart: this.onBeforeRestart.bind(this), onTerminated: this.onTerminated.bind(this), onTerminate: this.onTerminate.bind(this) } @@ -102,7 +106,7 @@ class ZapController { this.mainWindow.show() this.mainWindow.focus() - // Start the onboarding process. + // // Start the onboarding process. this.startOnboarding() }) @@ -137,6 +141,9 @@ class ZapController { stopLnd(...args: any[]) { return this.fsm.stopLnd(...args) } + restart(...args: any[]) { + return this.fsm.restart(...args) + } terminate(...args: any[]) { return this.fsm.terminate(...args) } @@ -171,9 +178,18 @@ class ZapController { } // Give the grpc connections a chance to be properly closed out. - return new Promise(resolve => setTimeout(resolve, 200)).then(() => - this.sendMessage('startOnboarding') - ) + await new Promise(resolve => setTimeout(resolve, 200)) + + if (lifecycle.transition === 'restart') { + this.mainWindow.reload() + } + } + + onStartOnboarding() { + mainLog.debug('[FSM] onStartOnboarding...') + + // Notify the app to start the onboarding process. + this.sendMessage('startOnboarding') } onBeforeStartLocalLnd() { @@ -230,6 +246,15 @@ class ZapController { }) } + onBeforeStopLnd() { + mainLog.debug('[FSM] onBeforeStopLnd...') + } + + onBeforeRestart() { + mainLog.debug('[FSM] onBeforeRestart...') + // this.mainWindow.reload() + } + async onTerminated(lifecycle: any) { mainLog.debug('[FSM] onTerminated...') @@ -321,7 +346,7 @@ class ZapController { * Starts the LND node and attach event listeners. * @return {Neutrino} Neutrino instance. */ - startNeutrino() { + async startNeutrino() { mainLog.info('Starting Neutrino...') this.neutrino = new Neutrino(this.lndConfig) @@ -335,7 +360,8 @@ class ZapController { this.neutrino.on('exit', (code, signal, lastError) => { mainLog.info(`Lnd process has shut down (code: ${code}, signal: ${signal})`) - if (this.is('running') || this.is('connected')) { + this.sendMessage('lndStopped') + if (this.is('running') || (this.is('connected') && !this.is('onboarding'))) { dialog.showMessageBox({ type: 'error', message: `Lnd has unexpectedly quit:\n\nError code: ${code}\nExit signal: ${signal}\nLast error: ${lastError}` @@ -376,7 +402,13 @@ class ZapController { this.sendMessage('lndCfilterHeight', Number(height)) }) - return this.neutrino.start() + try { + const pid = await this.neutrino.start() + this.sendMessage('lndStarted', this.lndConfig) + return pid + } catch (e) { + // console.error(e) + } } /** @@ -448,7 +480,13 @@ class ZapController { * Add IPC event listeners... */ _registerIpcListeners() { - ipcMain.on('startLnd', (event, options: onboardingOptions) => this.startLnd(options)) + ipcMain.on('startLnd', async (event, options: onboardingOptions) => { + try { + await this.startLnd(options) + } catch (e) { + mainLog.error('Unable to start lnd: %s', e.message) + } + }) ipcMain.on('startLightningWallet', () => this.startLightningWallet().catch(e => { // Notify the app of errors. @@ -459,6 +497,7 @@ class ZapController { }) ) ipcMain.on('stopLnd', () => this.stopLnd()) + ipcMain.on('restart', () => this.restart()) } /** @@ -467,6 +506,7 @@ class ZapController { _removeIpcListeners() { ipcMain.removeAllListeners('startLnd') ipcMain.removeAllListeners('stopLnd') + ipcMain.removeAllListeners('restart') ipcMain.removeAllListeners('startLightningWallet') ipcMain.removeAllListeners('walletUnlocker') ipcMain.removeAllListeners('lnd') diff --git a/app/reducers/index.js b/app/reducers/index.js index c3604e21..77068248 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -22,6 +22,7 @@ import network from './network' import error from './error' import loading from './loading' import settings from './settings' +import wallet from './wallet' export default history => combineReducers({ @@ -50,5 +51,6 @@ export default history => network, error, loading, - settings + settings, + wallet }) diff --git a/app/reducers/ipc.js b/app/reducers/ipc.js index e6a1dee3..7863b138 100644 --- a/app/reducers/ipc.js +++ b/app/reducers/ipc.js @@ -1,11 +1,19 @@ import createIpc from 'redux-electron-ipc' import { receiveLocale } from './locale' import { - lndSyncStatus, currentBlockHeight, + fetchSeedSuccess, + fetchSeedError, + lightningGrpcActive, + lndSyncStatus, + lndStopped, + lndStarted, lndBlockHeight, lndCfilterHeight, - lightningGrpcActive, + setUnlockWalletError, + startLndError, + walletCreated, + walletUnlocked, walletUnlockerGrpcActive } from './lnd' import { receiveInfo } from './info' @@ -40,15 +48,7 @@ import { import { receiveDescribeNetwork, receiveQueryRoutes, receiveInvoiceAndQueryRoutes } from './network' -import { - startOnboarding, - startLndError, - receiveSeed, - receiveSeedError, - walletCreated, - walletUnlocked, - setUnlockWalletError -} from './onboarding' +import { startOnboarding } from './onboarding' // Import all receiving IPC event handlers and pass them into createIpc const ipc = createIpc({ @@ -113,9 +113,11 @@ const ipc = createIpc({ startOnboarding, startLndError, + lndStopped, + lndStarted, walletUnlockerGrpcActive, - receiveSeed, - receiveSeedError, + fetchSeedSuccess, + fetchSeedError, walletCreated, walletUnlocked, setUnlockWalletError diff --git a/app/reducers/lnd.js b/app/reducers/lnd.js index 52c7e932..f2796e8f 100644 --- a/app/reducers/lnd.js +++ b/app/reducers/lnd.js @@ -1,9 +1,11 @@ +import { ipcRenderer } from 'electron' import { createSelector } from 'reselect' import { showNotification } from 'lib/utils/notifications' import db from 'store/db' import { fetchBalance } from './balance' import { fetchInfo, setHasSynced, infoSelectors } from './info' -import { lndWalletStarted, lndWalletUnlockerStarted } from './onboarding' +import { putWallet, setActiveWallet } from './wallet' +import { onboardingFinished, setSeed } from './onboarding' // ------------------------------------ // Constants @@ -20,6 +22,24 @@ export const RECEIVE_LND_CFILTER_HEIGHT = 'RECEIVE_LND_CFILTER_HEIGHT' export const SET_WALLET_UNLOCKER_ACTIVE = 'SET_WALLET_UNLOCKER_ACTIVE' export const SET_LIGHTNING_WALLET_ACTIVE = 'SET_LIGHTNING_WALLET_ACTIVE' +export const STARTING_LND = 'STARTING_LND' +export const LND_STARTED = 'LND_STARTED' +export const SET_START_LND_ERROR = 'SET_START_LND_ERROR' + +export const STOPPING_LND = 'STOPPING_LND' +export const LND_STOPPED = 'LND_STOPPED' + +export const CREATING_NEW_WALLET = 'CREATING_NEW_WALLET' +export const RECOVERING_OLD_WALLET = 'RECOVERING_OLD_WALLET' + +export const UNLOCKING_WALLET = 'UNLOCKING_WALLET' +export const WALLET_UNLOCKED = 'WALLET_UNLOCKED' +export const SET_UNLOCK_WALLET_ERROR = 'SET_UNLOCK_WALLET_ERROR' + +export const FETCH_SEED = 'FETCH_SEED' +export const FETCH_SEED_ERROR = 'FETCH_SEED_ERROR' +export const FETCH_SEED_SUCCESS = 'FETCH_SEED_SUCCESS' + // ------------------------------------ // Actions // ------------------------------------ @@ -66,17 +86,29 @@ export const lndSyncStatus = (event, status) => async (dispatch, getState) => { } // Connected to Lightning gRPC interface (lnd wallet is connected and unlocked) -export const lightningGrpcActive = (event, lndConfig) => dispatch => { +export const lightningGrpcActive = (event, lndConfig) => async dispatch => { dispatch({ type: SET_LIGHTNING_WALLET_ACTIVE }) + // Once we we have established a connection, save the wallet settings. + if (lndConfig.id !== 'tmp') { + const wallet = await dispatch(putWallet(lndConfig)) + dispatch(setActiveWallet(wallet.id)) + } + // Let the onboarding process know that wallet is active. dispatch(lndWalletStarted(lndConfig)) } // Connected to WalletUnlocker gRPC interface (lnd is ready to unlock or create wallet) -export const walletUnlockerGrpcActive = () => dispatch => { +export const walletUnlockerGrpcActive = (event, lndConfig) => async dispatch => { dispatch({ type: SET_WALLET_UNLOCKER_ACTIVE }) + // Once we we have established a connection, save the wallet settings. + if (lndConfig.id !== 'tmp') { + const wallet = await dispatch(putWallet(lndConfig)) + dispatch(setActiveWallet(wallet.id)) + } + // Let the onboarding process know that the wallet unlocker has started. dispatch(lndWalletUnlockerStarted()) } @@ -96,10 +128,228 @@ export const lndCfilterHeight = (event, height) => dispatch => { dispatch({ type: RECEIVE_LND_CFILTER_HEIGHT, lndCfilterHeight: height }) } +export const startLnd = options => async dispatch => { + return new Promise((resolve, reject) => { + // Tell the main process to start lnd using the supplied connection details. + dispatch({ type: STARTING_LND }) + ipcRenderer.send('startLnd', options) + + ipcRenderer.once('startLndError', error => { + ipcRenderer.removeListener('startLndSuccess', resolve) + reject(error) + }) + + ipcRenderer.once('startLndSuccess', res => { + ipcRenderer.removeListener('startLndError', reject) + resolve(res) + }) + }) +} + +// Listener for errors connecting to LND gRPC +export const startLndError = (event, errors) => dispatch => { + dispatch(setStartLndError(errors)) +} + +export function setStartLndError(errors) { + return { + type: SET_START_LND_ERROR, + errors + } +} + +export const stopLnd = () => async (dispatch, getState) => { + const state = getState().lnd + if (state.lndStarted && !state.stoppingLnd) { + dispatch({ type: STOPPING_LND }) + ipcRenderer.send('stopLnd') + } +} + +export const lndStopped = () => async dispatch => { + dispatch({ type: LND_STOPPED }) +} + +export const lndStarted = () => async dispatch => { + dispatch({ type: LND_STARTED }) +} + +export const unlockWallet = password => async dispatch => { + dispatch({ type: UNLOCKING_WALLET }) + ipcRenderer.send('walletUnlocker', { + msg: 'unlockWallet', + data: { wallet_password: password } + }) +} + +export const restart = () => () => { + ipcRenderer.send('restart') +} + +/** + * As soon as we have an active connection to a WalletUnlocker service, attempt to generate a new seed which kicks off + * the process of creating or unlocking a wallet. + */ +export const lndWalletUnlockerStarted = () => (dispatch, getState) => { + const state = getState().lnd + const onboardingState = getState().onboarding + + // Handle generate seed. + if (state.fetchingSeed) { + ipcRenderer.send('walletUnlocker', { msg: 'genSeed' }) + } + + // Handle unlock wallet. + else if (state.unlockingWallet) { + ipcRenderer.send('walletUnlocker', { + msg: 'unlockWallet', + data: { wallet_password: onboardingState.password } + }) + } + + // Handle create wallet. + else if (state.creatingNewWallet) { + ipcRenderer.send('walletUnlocker', { + msg: 'initWallet', + data: { + wallet_password: onboardingState.password, + cipher_seed_mnemonic: onboardingState.seed + } + }) + } + + // Handle recover wallet. + else if (state.recoveringOldWallet) { + ipcRenderer.send('walletUnlocker', { + msg: 'initWallet', + data: { + wallet_password: onboardingState.password, + cipher_seed_mnemonic: onboardingState.seed, + recovery_window: 250 + } + }) + } +} + +export const walletCreated = () => dispatch => { + dispatch({ type: WALLET_UNLOCKED }) + dispatch(onboardingFinished()) + ipcRenderer.send('startLightningWallet') +} + +export const walletUnlocked = () => dispatch => { + dispatch({ type: WALLET_UNLOCKED }) + dispatch(onboardingFinished()) + ipcRenderer.send('startLightningWallet') +} + +export const setUnlockWalletError = (event, unlockWalletError) => dispatch => { + dispatch({ type: SET_UNLOCK_WALLET_ERROR, unlockWalletError }) +} + +export const fetchSeed = () => async dispatch => { + dispatch({ type: FETCH_SEED }) + await dispatch( + startLnd({ + id: `tmp`, + type: 'local', + chain: 'bitcoin', + network: 'testnet' + }) + ) +} + +// Listener for when LND creates and sends us a generated seed +export const fetchSeedSuccess = (event, { cipher_seed_mnemonic }) => dispatch => { + dispatch({ type: FETCH_SEED_SUCCESS, seed: cipher_seed_mnemonic }) + dispatch(setSeed(cipher_seed_mnemonic)) + dispatch(stopLnd()) +} + +// Listener for when LND throws an error on seed creation +export const fetchSeedError = (event, error) => dispatch => { + dispatch({ type: FETCH_SEED_ERROR, error }) +} + +export const createNewWallet = () => async (dispatch, getState) => { + const onboardingState = getState().onboarding + + // Define the wallet config. + let wallet = { + type: 'local', + chain: 'bitcoin', + network: 'testnet', + settings: { + autopilot: onboardingState.autopilot, + alias: onboardingState.alias + } + } + + // Save the wallet config. + wallet = await dispatch(putWallet(wallet)) + + // Start Lnd and trigger the wallet to be initialised as soon as the wallet unlocker is available. + dispatch({ type: CREATING_NEW_WALLET }) + await dispatch(startLnd(wallet)) +} + +export const recoverOldWallet = () => async dispatch => { + // Define the wallet config. + let wallet = { + type: 'local', + chain: 'bitcoin', + network: 'testnet' + } + + // Save the wallet config. + wallet = await dispatch(putWallet(wallet)) + + // Start Lnd and trigger the wallet to be recovered as soon as the wallet unlocker is available. + dispatch({ type: RECOVERING_OLD_WALLET }) + await dispatch(startLnd(wallet)) +} + +export const startActiveWallet = () => async (dispatch, getState) => { + const state = getState().lnd + if (!state.lndStarted && !state.startingLnd) { + const activeWallet = await db.settings.get({ key: 'activeWallet' }) + if (activeWallet) { + const wallet = await db.wallets.get({ id: activeWallet.value }) + if (wallet) { + dispatch(startLnd(wallet)) + } + } + } +} + +/** + * 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 = () => async dispatch => { + // Fetch info from lnd. + dispatch(fetchInfo()) + + // Let the onboarding process know that the wallet has started. + dispatch(onboardingFinished()) +} + // ------------------------------------ // Action Handlers // ------------------------------------ const ACTION_HANDLERS = { + [FETCH_SEED]: state => ({ ...state, fetchingSeed: true }), + [FETCH_SEED_SUCCESS]: state => ({ + ...state, + fetchingSeed: false, + fetchSeedError: '' + }), + [FETCH_SEED_ERROR]: (state, { error }) => ({ + ...state, + fetchingSeed: false, + fetchSeedError: error + }), + [SET_SYNC_STATUS_PENDING]: state => ({ ...state, syncStatus: 'pending' }), [SET_SYNC_STATUS_WAITING]: state => ({ ...state, syncStatus: 'waiting' }), [SET_SYNC_STATUS_IN_PROGRESS]: state => ({ ...state, syncStatus: 'in-progress' }), @@ -112,6 +362,23 @@ const ACTION_HANDLERS = { [RECEIVE_LND_BLOCK_HEIGHT]: (state, { lndBlockHeight }) => ({ ...state, lndBlockHeight }), [RECEIVE_LND_CFILTER_HEIGHT]: (state, { lndCfilterHeight }) => ({ ...state, lndCfilterHeight }), + [STARTING_LND]: state => ({ + ...state, + startingLnd: true, + lndStarted: false + }), + [LND_STARTED]: state => ({ + ...state, + startingLnd: false, + lndStarted: true + }), + [SET_START_LND_ERROR]: (state, { errors }) => ({ + ...state, + startLndHostError: errors.host, + startLndCertError: errors.cert, + startLndMacaroonError: errors.macaroon + }), + [SET_WALLET_UNLOCKER_ACTIVE]: state => ({ ...state, walletUnlockerGrpcActive: true, @@ -121,6 +388,29 @@ const ACTION_HANDLERS = { ...state, lightningGrpcActive: true, walletUnlockerGrpcActive: false + }), + + [STOPPING_LND]: state => ({ + ...state, + stoppingLnd: true + }), + [LND_STOPPED]: state => ({ + ...state, + ...initialState + }), + + [CREATING_NEW_WALLET]: state => ({ ...state, creatingNewWallet: true }), + [RECOVERING_OLD_WALLET]: state => ({ ...state, recoveringOldWallet: true }), + [UNLOCKING_WALLET]: state => ({ ...state, unlockingWallet: true }), + [WALLET_UNLOCKED]: state => ({ + ...state, + unlockingWallet: false, + unlockWalletError: '' + }), + [SET_UNLOCK_WALLET_ERROR]: (state, { unlockWalletError }) => ({ + ...state, + unlockingWallet: false, + unlockWalletError }) } @@ -128,16 +418,28 @@ const ACTION_HANDLERS = { // Reducer // ------------------------------------ const initialState = { - syncStatus: 'pending', + fetchingSeed: false, + startingLnd: false, + stoppingLnd: false, + lndStarted: false, + creatingNewWallet: false, + recoveringOldWallet: false, + unlockingWallet: false, walletUnlockerGrpcActive: false, lightningGrpcActive: false, + unlockWalletError: '', + startLndHostError: '', + startLndCertError: '', + startLndMacaroonError: '', + fetchSeedError: '', + syncStatus: 'pending', blockHeight: 0, lndBlockHeight: 0, lndCfilterHeight: 0 } // ------------------------------------ -// Reducer +// Selectors // ------------------------------------ const lndSelectors = {} const blockHeightSelector = state => state.lnd.blockHeight @@ -163,6 +465,10 @@ lndSelectors.syncPercentage = createSelector( export { lndSelectors } +// ------------------------------------ +// Reducer +// ------------------------------------ +// export default function lndReducer(state = initialState, action) { const handler = ACTION_HANDLERS[action.type] diff --git a/app/reducers/onboarding.js b/app/reducers/onboarding.js index ad331a63..3c6b7770 100644 --- a/app/reducers/onboarding.js +++ b/app/reducers/onboarding.js @@ -1,16 +1,15 @@ -import crypto from 'crypto' import { createSelector } from 'reselect' -import { ipcRenderer } from 'electron' import get from 'lodash.get' import db from 'store/db' import { validateHost as doHostValidation } from 'lib/utils/validateHost' import { fileExists } from 'lib/utils/fileExists' -import { fetchInfo } from './info' -import { setError } from './error' +import { setStartLndError } from './lnd' // ------------------------------------ // Constants // ------------------------------------ +export const ONBOARDING_STARTED = 'ONBOARDING_STARTED' +export const ONBOARDING_FINISHED = 'ONBOARDING_FINISHED' export const SET_CONNECTION_TYPE = 'SET_CONNECTION_TYPE' export const SET_CONNECTION_STRING = 'SET_CONNECTION_STRING' export const SET_CONNECTION_HOST = 'SET_CONNECTION_HOST' @@ -19,34 +18,10 @@ export const SET_CONNECTION_MACAROON = 'SET_CONNECTION_MACAROON' export const SET_ALIAS = 'SET_ALIAS' export const SET_AUTOPILOT = 'SET_AUTOPILOT' export const SET_PASSWORD = 'SET_PASSWORD' -export const SET_LND_WALLET_UNLOCKER_STARTED = 'SET_LND_WALLET_UNLOCKER_STARTED' -export const SET_LND_WALLET_STARTED = 'SET_LND_WALLET_STARTED' - -export const FETCH_SEED = 'FETCH_SEED' export const SET_SEED = 'SET_SEED' - -export const ONBOARDING_STARTED = 'ONBOARDING_STARTED' -export const ONBOARDING_FINISHED = 'ONBOARDING_FINISHED' - -export const STARTING_LND = 'STARTING_LND' -export const LND_STARTED = 'LND_STARTED' -export const SET_START_LND_ERROR = 'SET_START_LND_ERROR' - -export const STOPPING_LND = 'STOPPING_LND' -export const LND_STOPPED = 'LND_STOPPED' - -export const LOADING_EXISTING_WALLET = 'LOADING_EXISTING_WALLET' -export const CREATING_NEW_WALLET = 'CREATING_NEW_WALLET' -export const RECOVERING_OLD_WALLET = 'RECOVERING_OLD_WALLET' - -export const UNLOCKING_WALLET = 'UNLOCKING_WALLET' -export const WALLET_UNLOCKED = 'WALLET_UNLOCKED' -export const SET_UNLOCK_WALLET_ERROR = 'SET_UNLOCK_WALLET_ERROR' - export const VALIDATING_HOST = 'VALIDATING_HOST' export const VALIDATING_CERT = 'VALIDATING_CERT' export const VALIDATING_MACAROON = 'VALIDATING_MACAROON' - export const RESET_ONBOARDING = 'RESET_ONBOARDING' // ------------------------------------ @@ -68,6 +43,18 @@ export const resetOnboarding = () => dispatch => { dispatch({ type: SET_SEED, seed: [] }) } +export function onboardingStarted() { + return { + type: ONBOARDING_STARTED + } +} + +export function onboardingFinished() { + return { + type: ONBOARDING_FINISHED + } +} + export const setConnectionType = connectionType => async (dispatch, getState) => { const previousType = connectionTypeSelector(getState()) @@ -142,15 +129,10 @@ export function setPassword(password) { } } -export function setLndWalletUnlockerStarted() { - return { - type: SET_LND_WALLET_UNLOCKER_STARTED - } -} - -export function setLndWalletStarted() { +export function setSeed(seed) { return { - type: SET_LND_WALLET_STARTED + type: SET_SEED, + seed } } @@ -196,234 +178,8 @@ export const validateMacaroon = macaroonPath => async dispatch => { } } -export const startLnd = options => async dispatch => { - return new Promise((resolve, reject) => { - // Tell the main process to start lnd using the supplied connection details. - dispatch({ type: STARTING_LND }) - ipcRenderer.send('startLnd', options) - - ipcRenderer.once('startLndError', error => { - ipcRenderer.removeListener('startLndSuccess', resolve) - reject(error) - }) - - ipcRenderer.once('startLndSuccess', res => { - ipcRenderer.removeListener('startLndError', reject) - resolve(res) - }) - }) -} - -// Listener for errors connecting to LND gRPC -export const startLndError = (event, errors) => (dispatch, getState) => { - const connectionType = connectionTypeSelector(getState()) - switch (connectionType) { - case 'custom': - dispatch(setStartLndError(errors)) - break - case 'btcpayserver': - dispatch(setStartLndError(errors)) - break - default: - dispatch(setError(errors)) - } -} - -export const lndStarted = () => async dispatch => { - dispatch({ type: LND_STARTED }) -} - -export function setStartLndError(errors) { - return { - type: SET_START_LND_ERROR, - errors - } -} - -export const stopLnd = () => async dispatch => { - dispatch({ type: STOPPING_LND }) - ipcRenderer.send('stopLnd') -} - -export const lndStopped = () => async dispatch => { - dispatch({ type: LND_STOPPED }) -} - -export const generateSeed = () => async dispatch => { - dispatch({ type: FETCH_SEED }) - ipcRenderer.send('startLnd', { - id: `tmp`, - type: 'local', - chain: 'bitcoin', - network: 'testnet' - }) -} - -export const createNewWallet = () => (dispatch, getState) => { - crypto.randomBytes(16, async (err, buffer) => { - const state = getState().onboarding - - // Define the wallet config. - const wallet = { - id: buffer.toString('hex'), - type: 'local', - chain: 'bitcoin', - network: 'testnet', - settings: { - autopilot: state.autopilot, - alias: state.alias - } - } - - // Save the wallet config. - await db.wallets.put(wallet) - - // Start Lnd and trigger the wallet to be initialised as soon as the wallet unlocker is available. - dispatch({ type: CREATING_NEW_WALLET }) - ipcRenderer.send('startLnd', wallet) - }) -} - -export const recoverOldWallet = () => dispatch => { - crypto.randomBytes(16, function(err, buffer) { - const id = buffer.toString('hex') - - dispatch({ type: RECOVERING_OLD_WALLET }) - ipcRenderer.send('startLnd', { - id, - type: 'local', - chain: 'bitcoin', - network: 'testnet' - }) - }) -} - -export const startActiveWallet = () => async dispatch => { - const activeWallet = await db.settings.get({ key: 'activeWallet' }) - if (activeWallet) { - const wallet = await db.wallets.get({ id: activeWallet.value }) - if (wallet) { - dispatch(startLnd(wallet)) - } - } -} - -export const unlockWallet = password => async dispatch => { - dispatch({ type: UNLOCKING_WALLET }) - ipcRenderer.send('walletUnlocker', { - msg: 'unlockWallet', - data: { wallet_password: password } - }) -} - -/** - * As soon as we have an active connection to a WalletUnlocker service, attempt to generate a new seed which kicks off - * the process of creating or unlocking a wallet. - */ -export const lndWalletUnlockerStarted = () => (dispatch, getState) => { - dispatch(setLndWalletUnlockerStarted('active')) - const state = getState().onboarding - - // Handle generate seed. - if (state.fetchingSeed) { - ipcRenderer.send('walletUnlocker', { msg: 'genSeed' }) - } - - // Handle unlock wallet. - else if (state.unlockingWallet) { - ipcRenderer.send('walletUnlocker', { - msg: 'unlockWallet', - data: { wallet_password: state.password } - }) - } - - // Handle create wallet. - else if (state.creatingNewWallet) { - ipcRenderer.send('walletUnlocker', { - msg: 'initWallet', - data: { wallet_password: state.password, cipher_seed_mnemonic: state.seed } - }) - } - - // Handle recover wallet. - else if (state.recoveringOldWallet) { - ipcRenderer.send('walletUnlocker', { - msg: 'initWallet', - data: { - wallet_password: state.password, - cipher_seed_mnemonic: state.seed, - recovery_window: 250 - } - }) - } - - // // Handle remote connect. - // else if (state.startingLnd) { - // ipcRenderer.send('walletUnlocker', { - // msg: 'unlockWallet', - // data: { wallet_password: state.password } - // }) - // } -} - -/** - * 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 = lndConfig => async dispatch => { - dispatch(setLndWalletStarted()) - - // 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(lndConfig)) - dispatch({ type: ONBOARDING_FINISHED }) -} - -// Listener for errors connecting to LND gRPC -export const startOnboarding = () => async (dispatch, getState) => { - const state = getState().onboarding - if (state.stoppingLnd) { - dispatch(lndStopped()) - } - dispatch({ type: ONBOARDING_STARTED }) -} - -// Listener for when LND creates and sends us a generated seed -export const receiveSeed = (event, { cipher_seed_mnemonic }) => dispatch => { - dispatch({ type: SET_SEED, seed: cipher_seed_mnemonic }) - dispatch(stopLnd()) -} - -// Listener for when LND throws an error on seed creation -export const receiveSeedError = (event, error) => dispatch => { - dispatch({ - type: LOADING_EXISTING_WALLET, - existingWalletDir: get(error, 'context.lndDataDir') - }) -} - -export const walletCreated = () => dispatch => { - dispatch({ type: WALLET_UNLOCKED }) - dispatch({ type: ONBOARDING_FINISHED }) - ipcRenderer.send('startLightningWallet') -} - -export const walletUnlocked = () => dispatch => { - dispatch({ type: WALLET_UNLOCKED }) - dispatch({ type: ONBOARDING_FINISHED }) - ipcRenderer.send('startLightningWallet') -} - -export const setUnlockWalletError = (event, unlockWalletError) => dispatch => { - dispatch({ type: SET_UNLOCK_WALLET_ERROR, unlockWalletError }) +export const startOnboarding = () => dispatch => { + dispatch(onboardingStarted()) } // ------------------------------------ @@ -437,63 +193,10 @@ const ACTION_HANDLERS = { [SET_CONNECTION_MACAROON]: (state, { connectionMacaroon }) => ({ ...state, connectionMacaroon }), [SET_ALIAS]: (state, { alias }) => ({ ...state, alias }), [SET_AUTOPILOT]: (state, { autopilot }) => ({ ...state, autopilot }), - [FETCH_SEED]: state => ({ ...state, fetchingSeed: true }), [SET_SEED]: (state, { seed }) => ({ ...state, seed, fetchingSeed: false }), [SET_PASSWORD]: (state, { password }) => ({ ...state, password }), - [SET_LND_WALLET_UNLOCKER_STARTED]: state => ({ - ...state, - lndWalletUnlockerStarted: true, - lndWalletStarted: false - }), - [SET_LND_WALLET_STARTED]: state => ({ - ...state, - lndWalletStarted: true, - lndWalletUnlockerStarted: false - }), [ONBOARDING_STARTED]: state => ({ ...state, onboarding: true, onboarded: false }), [ONBOARDING_FINISHED]: state => ({ ...state, onboarding: false, onboarded: true }), - [STARTING_LND]: state => ({ - ...state, - startingLnd: true, - startLndHostError: '', - startLndCertError: '', - startLndMacaroonError: '' - }), - [LND_STARTED]: state => ({ - ...state, - startingLnd: false, - startLndHostError: '', - startLndCertError: '', - startLndMacaroonError: '' - }), - [SET_START_LND_ERROR]: (state, { errors }) => ({ - ...state, - startingLnd: false, - startLndHostError: errors.host, - startLndCertError: errors.cert, - startLndMacaroonError: errors.macaroon - }), - [STOPPING_LND]: state => ({ - ...state, - stoppingLnd: true, - lndWalletStarted: false, - lndWalletUnlockerStarted: false - }), - [LND_STOPPED]: state => ({ ...state, stoppingLnd: false }), - [LOADING_EXISTING_WALLET]: (state, { existingWalletDir }) => ({ ...state, existingWalletDir }), - [CREATING_NEW_WALLET]: state => ({ ...state, creatingNewWallet: true }), - [RECOVERING_OLD_WALLET]: state => ({ ...state, recoveringOldWallet: true }), - [UNLOCKING_WALLET]: state => ({ ...state, unlockingWallet: true }), - [WALLET_UNLOCKED]: state => ({ - ...state, - unlockingWallet: false, - unlockWalletError: '' - }), - [SET_UNLOCK_WALLET_ERROR]: (state, { unlockWalletError }) => ({ - ...state, - unlockingWallet: false, - unlockWalletError - }), [VALIDATING_HOST]: (state, { validatingHost }) => ({ ...state, validatingHost }), [VALIDATING_CERT]: (state, { validatingCert }) => ({ ...state, validatingCert }), [VALIDATING_MACAROON]: (state, { validatingMacaroon }) => ({ ...state, validatingMacaroon }), @@ -509,8 +212,6 @@ const connectionStringSelector = state => state.onboarding.connectionString const connectionTypeSelector = state => state.onboarding.connectionType -onboardingSelectors.startingLnd = state => state.onboarding.startingLnd - onboardingSelectors.connectionStringParamsSelector = createSelector( connectionStringSelector, connectionString => { @@ -535,30 +236,18 @@ export { onboardingSelectors } const initialState = { onboarding: false, onboarded: false, + autopilot: true, + validatingHost: false, + validatingCert: false, + validatingMacaroon: false, connectionType: 'create', connectionString: '', connectionHost: '', connectionCert: '', connectionMacaroon: '', alias: '', - autopilot: true, password: '', - startingLnd: false, - startLndHostError: '', - startLndCertError: '', - startLndMacaroonError: '', - fetchingSeed: false, - seed: [], - creatingNewWallet: false, - recoveringOldWallet: false, - existingWalletDir: null, - unlockingWallet: false, - unlockWalletError: '', - validatingHost: false, - validatingCert: false, - validatingMacaroon: false, - lndWalletUnlockerStarted: false, - lndWalletStarted: false + seed: [] } // ------------------------------------ diff --git a/app/reducers/wallet.js b/app/reducers/wallet.js new file mode 100644 index 00000000..5ecbdce1 --- /dev/null +++ b/app/reducers/wallet.js @@ -0,0 +1,132 @@ +import { createSelector } from 'reselect' +import db from 'store/db' + +// ------------------------------------ +// Constants +// ------------------------------------ +export const SET_WALLETS = 'SET_WALLETS' +export const SET_ACTIVE_WALLET = 'SET_ACTIVE_WALLET' +export const SET_IS_WALLET_OPEN = 'SET_IS_WALLET_OPEN' +export const DELETE_WALLET = 'DELETE_WALLET' +export const PUT_WALLET = 'PUT_WALLET' + +// ------------------------------------ +// Actions +// ------------------------------------ + +export function setWallets(wallets) { + return { + type: SET_WALLETS, + wallets + } +} + +export function setIsWalletOpen(isWalletOpen) { + db.settings.put({ + key: 'isWalletOpen', + value: isWalletOpen + }) + return { + type: SET_IS_WALLET_OPEN, + isWalletOpen + } +} + +export function setActiveWallet(activeWallet) { + db.settings.put({ + key: 'activeWallet', + value: activeWallet + }) + return { + type: SET_ACTIVE_WALLET, + activeWallet + } +} + +export const getWallets = () => async dispatch => { + let wallets + try { + wallets = await db.wallets.toArray() + } catch (e) { + wallets = [] + } + dispatch(setWallets(wallets)) + return wallets +} + +export const putWallet = wallet => async dispatch => { + dispatch({ type: PUT_WALLET, wallet }) + wallet.id = await db.wallets.put(wallet) + await dispatch(getWallets()) + return wallet +} + +export const deleteWallet = walletId => async dispatch => { + dispatch({ type: DELETE_WALLET, walletId }) + await db.wallets.delete(walletId) + const wallets = await dispatch(getWallets()) + dispatch(setActiveWallet(wallets[0].id)) + setIsWalletOpen(false) +} + +export const initWallets = () => async dispatch => { + let activeWallet + let isWalletOpen + try { + await dispatch(getWallets()) + activeWallet = await db.settings.get({ key: 'activeWallet' }) + activeWallet = activeWallet.value || null + + isWalletOpen = await db.settings.get({ key: 'isWalletOpen' }) + isWalletOpen = isWalletOpen.value + } catch (e) { + activeWallet = null + isWalletOpen = false + } + dispatch(setIsWalletOpen(isWalletOpen)) + dispatch(setActiveWallet(activeWallet)) +} + +// ------------------------------------ +// Action Handlers +// ------------------------------------ +const ACTION_HANDLERS = { + [SET_WALLETS]: (state, { wallets }) => ({ ...state, wallets }), + [SET_ACTIVE_WALLET]: (state, { activeWallet }) => ({ ...state, activeWallet }), + [SET_IS_WALLET_OPEN]: (state, { isWalletOpen }) => ({ ...state, isWalletOpen }) +} + +// ------------------------------------ +// Selectors +// ------------------------------------ + +const walletSelectors = {} +const activeWalletSelector = state => state.wallet.activeWallet +const walletsSelector = state => state.wallet.wallets + +walletSelectors.wallets = createSelector(walletsSelector, wallets => wallets) +walletSelectors.activeWallet = createSelector(activeWalletSelector, activeWallet => activeWallet) +walletSelectors.activeWalletSettings = createSelector( + walletsSelector, + activeWalletSelector, + (wallets, activeWallet) => wallets.find(wallet => wallet.id === activeWallet) +) +walletSelectors.hasWallets = createSelector(walletSelectors.wallets, wallets => wallets.length > 0) + +export { walletSelectors } + +// ------------------------------------ +// Reducer +// ------------------------------------ + +const initialState = { + isWalletOpen: false, + activeWallet: undefined, + wallets: [] +} + +export default function walletReducer(state = initialState, action) { + const handler = ACTION_HANDLERS[action.type] + + return handler ? handler(state, action) : state +} diff --git a/app/store/configureStore.prod.js b/app/store/configureStore.prod.js index 90d85745..df1ed4c5 100644 --- a/app/store/configureStore.prod.js +++ b/app/store/configureStore.prod.js @@ -1,11 +1,11 @@ import { createStore, applyMiddleware, compose } from 'redux' import thunk from 'redux-thunk' -import { createHashHistory } from 'history' +import { createMemoryHistory } from 'history' import { routerMiddleware } from 'connected-react-router' import createRootReducer from '../reducers' import ipc from '../reducers/ipc' -export const history = createHashHistory({ basename: window.location.pathname }) +export const history = createMemoryHistory({ basename: window.location.pathname }) export function configureStore(initialState) { const middleware = [] diff --git a/stories/containers/home.stories.js b/stories/containers/home.stories.js new file mode 100644 index 00000000..8ba4aa4a --- /dev/null +++ b/stories/containers/home.stories.js @@ -0,0 +1,112 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { linkTo } from '@storybook/addon-links' +import { State, Store } from '@sambego/storybook-state' +import StoryRouter from 'storybook-react-router' +import { Page } from 'components/UI' +import { Home } from 'components/Home' + +const delay = time => new Promise(resolve => setTimeout(() => resolve(), time)) + +const store = new Store({ + activeWallet: 1, + lightningGrpcActive: false, + walletUnlockerGrpcActive: false, + unlockingWallet: false, + unlockWalletError: '', + wallets: [ + { + id: 1, + autopilot: true, + autopilotAllocation: 0.6, + autopilotMaxchannels: 5, + autopilotMaxchansize: 16777215, + autopilotMinchansize: 20000, + autopilotMinconfs: 0, + autopilotPrivate: true, + chain: 'bitcoin', + network: 'testnet', + type: 'local' + }, + { + id: 2, + autopilot: true, + autopilotAllocation: 0.9, + autopilotMaxchannels: 10, + autopilotMaxchansize: 16777215, + autopilotMinchansize: 20000, + autopilotMinconfs: 1, + autopilotPrivate: false, + chain: 'bitcoin', + network: 'mainnet', + type: 'local' + }, + { + id: 3, + type: 'custom', + chain: 'bitcoin', + network: 'testnet', + host: 'mynode.local' + }, + { + id: 4, + alias: 'The Lightning Store', + type: 'btcpayserver', + chain: 'bitcoin', + network: 'testnet', + host: 'example.btcpay.store' + } + ] +}) + +const startLnd = async wallet => { + console.log('startLnd', wallet) + await delay(500) + store.set({ walletUnlockerGrpcActive: true, lightningGrpcActive: false }) +} +const stopLnd = async () => { + console.log('stopLnd') + await delay(500) + store.set({ walletUnlockerGrpcActive: false, lightningGrpcActive: false }) +} +const unlockWallet = async (wallet, password) => { + console.log('unlockWallet', wallet, password) + await delay(300) + store.set({ walletUnlockerGrpcActive: false, lightningGrpcActive: true }) +} +const deleteWallet = async walletId => { + console.log('deleteWallet', walletId) + await delay(200) +} +const setUnlockWalletError = async unlockWalletError => store.set({ unlockWalletError }) +const setActiveWallet = async activeWallet => store.set({ activeWallet }) + +storiesOf('Containers.Home', module) + .addParameters({ + info: { + disable: true + } + }) + .addDecorator( + StoryRouter({ + '/onboarding': linkTo('Containers.Onboarding', 'Onboarding'), + '/syncing': linkTo('Containers.Syncing', 'Syncing'), + '/app': linkTo('Containers.App', 'App') + }) + ) + .add('Home', () => { + return ( + + + + + + ) + }) diff --git a/stories/containers/onboarding.stories.js b/stories/containers/onboarding.stories.js index 828b8b5f..4d01a077 100644 --- a/stories/containers/onboarding.stories.js +++ b/stories/containers/onboarding.stories.js @@ -50,7 +50,7 @@ const resetOnboarding = () => { store.set(initialValues) } -const generateSeed = async () => { +const fetchSeed = async () => { await delay(1000) store.set({ seed: [ @@ -148,7 +148,7 @@ storiesOf('Containers.Onboarding', module) validateHost={validateHost} validateCert={validateCert} validateMacaroon={validateMacaroon} - generateSeed={generateSeed} + fetchSeed={fetchSeed} /> )