Browse Source

feat(home): implement new Home screen

next
Tom Kirkpatrick 6 years ago
parent
commit
0248bc8f59
No known key found for this signature in database GPG Key ID: 72203A8EC5967EA8
  1. 8
      app/components/App/App.js
  2. 18
      app/components/Home/CreateWalletButton.js
  3. 126
      app/components/Home/Home.js
  4. 18
      app/components/Home/WalletHeader.js
  5. 117
      app/components/Home/WalletLauncher.js
  6. 221
      app/components/Home/WalletSettingsFormLocal.js
  7. 39
      app/components/Home/WalletSettingsFormRemote.js
  8. 130
      app/components/Home/WalletUnlocker.js
  9. 60
      app/components/Home/WalletsMenu.js
  10. 8
      app/components/Home/index.js
  11. 22
      app/components/Onboarding/Onboarding.js
  12. 8
      app/components/Onboarding/Steps/ConnectionConfirm.js
  13. 2
      app/components/Onboarding/Steps/Login.js
  14. 4
      app/components/Onboarding/Steps/SeedConfirm.js
  15. 8
      app/components/Onboarding/Steps/SeedView.js
  16. 3
      app/components/Onboarding/Steps/messages.js
  17. 6
      app/containers/App.js
  18. 28
      app/containers/Home.js
  19. 92
      app/containers/Initializer.js
  20. 44
      app/containers/Logout.js
  21. 31
      app/containers/Onboarding.js
  22. 44
      app/containers/Root.js
  23. 64
      app/lib/lnd/config.js
  24. 21
      app/lib/lnd/neutrino.js
  25. 4
      app/lib/lnd/walletUnlockerMethods/index.js
  26. 56
      app/lib/zap/controller.js
  27. 4
      app/reducers/index.js
  28. 28
      app/reducers/ipc.js
  29. 316
      app/reducers/lnd.js
  30. 361
      app/reducers/onboarding.js
  31. 132
      app/reducers/wallet.js
  32. 4
      app/store/configureStore.prod.js
  33. 112
      stories/containers/home.stories.js
  34. 4
      stories/containers/onboarding.stories.js

8
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()
}

18
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 }) => (
<Button {...rest} variant="secondary">
<Flex alignItem="center">
<Text color="lightningOrange">
<PlusCircle width="22px" height="22px" />
</Text>
<Text lineHeight="22px" ml={2}>
Create new wallet
</Text>
</Flex>
</Button>
)
export default CreateWalletButton

126
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 = () => (
<Box>
<Heading>Please select a wallet</Heading>
</Box>
)
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 (
<>
<Sidebar.small p={3} pt={40}>
<ZapLogo width="70px" height="32px" />
<WalletsMenu
wallets={wallets}
mt={30}
activeWallet={activeWallet}
setActiveWallet={setActiveWallet}
/>
<Box width={1} css={{ position: 'absolute', left: 0, bottom: 0 }} px={3}>
<Bar mx={-3} />
<CreateWalletButton onClick={this.handleCreateNewWalletClick} width={1} p={3} />
</Box>
</Sidebar.small>
<MainContent width={1 / 2}>
<Box px={3} mt={72}>
<Switch>
<Route
exact
path="/home/wallet/:walletId"
render={({ match: { params } }) => {
const wallet = wallets.find(wallet => wallet.id == params.walletId)
if (!wallet) {
return null
}
return (
<WalletLauncher
wallet={wallet}
startLnd={startLnd}
stopLnd={stopLnd}
lightningGrpcActive={lightningGrpcActive}
walletUnlockerGrpcActive={walletUnlockerGrpcActive}
deleteWallet={deleteWallet}
/>
)
}}
/>
<Route
exact
path="/home/wallet/:walletId/unlock"
render={({ match: { params } }) => {
const wallet = wallets.find(wallet => wallet.id == params.walletId)
if (!wallet) {
return null
}
return (
<WalletUnlocker
wallet={wallet}
unlockWallet={unlockWallet}
lightningGrpcActive={lightningGrpcActive}
setUnlockWalletError={setUnlockWalletError}
unlockingWallet={unlockingWallet}
unlockWalletError={unlockWalletError}
/>
)
}}
/>
<Route component={NoMatch} />
</Switch>
</Box>
</MainContent>
</>
)
}
}
export default withRouter(Home)

18
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 }) => (
<Box>
<Heading.h1 fontSize="xxl">
<Truncate text={title} maxlen={25} />
</Heading.h1>
</Box>
)
WalletHeader.propTypes = {
title: PropTypes.string.isRequired
}
export default WalletHeader

117
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 (
<React.Fragment>
<Flex mb={4} alignItems="center">
<WalletHeader title={walletName} />
<Box ml="auto">
<Button type="button" size="small" onClick={this.handleDelete} mr={2}>
delete
</Button>
<Button
type="submit"
size="small"
variant="primary"
form={`wallet-settings-form-${wallet.id}`}
>
<Flex>
<Box mr={1}>Launch now</Box>
<Box>
<ArrowRight />
</Box>
</Flex>
</Button>
</Box>
</Flex>
{wallet.type === 'local' && (
<>
<Heading.h2 mb={4}>Settings</Heading.h2>
<Bar my={2} />
<WalletSettingsFormLocal
key={wallet.id}
id={`wallet-settings-form-${wallet.id}`}
wallet={wallet}
startLnd={startLnd}
/>
</>
)}
{wallet.type !== 'local' && (
<>
<WalletSettingsFormRemote
key={wallet.id}
id={`wallet-settings-form-${wallet.id}`}
wallet={wallet}
startLnd={startLnd}
/>
</>
)}
</React.Fragment>
)
}
}
export default withRouter(WalletLauncher)

221
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 (
<Form
getApi={this.setFormApi}
preSubmit={this.preSubmit}
onSubmit={this.onSubmit}
{...rest}
initialValues={wallet}
wallet={wallet}
>
{({ formState }) => (
<React.Fragment>
<DataRow
py={2}
left={<Label htmlFor="alias">Alias</Label>}
right={<Input field="alias" id="alias" initialValue={wallet.alias} width={1} />}
/>
<DataRow
py={2}
left={<Label htmlFor="autopilot">Autopilot</Label>}
right={
<Toggle
field="autopilot"
id="autopilot"
validate={this.validateAutopilot}
validateOnBlur
validateOnChange={formState.invalid}
initialValue={wallet.autopilot}
/>
}
/>
{formState.values.autopilot ? (
<React.Fragment>
<DataRow
py={2}
left={<Label htmlFor="autopilotAllocation">Percentage of Balance</Label>}
right={
<Flex alignItems="center" ml="auto">
<Range
field="autopilotAllocation"
id="autopilotAllocation"
initialValue={wallet.autopilotAllocation * 100}
validate={this.validateAutopilotAllocation}
validateOnChange={formState.invalid}
validateOnBlur
ml="auto"
min="0"
max="100"
step="1"
width={1}
/>
<Input
field="autopilotAllocation"
id="autopilotAllocation"
type="number"
variant="thin"
ml={2}
width={100}
/>
</Flex>
}
/>
<DataRow
py={2}
left={<Label htmlFor="autopilotMaxchannels">Number of Channels max</Label>}
right={
<Input
field="autopilotMaxchannels"
id="autopilotMaxchannels"
variant="thin"
type="number"
initialValue={wallet.autopilotMaxchannels}
validate={this.validateAutopilotMaxchannels}
validateOnChange={formState.invalid}
validateOnBlur
step="1"
width={100}
ml="auto"
/>
}
/>
<DataRow
py={2}
left={<Label htmlFor="autopilotMinchansize">Minimum channel size</Label>}
right={
<Input
field="autopilotMinchansize"
id="autopilotMinchansize"
variant="thin"
type="number"
min="0"
max="100000000"
step="1"
initialValue={wallet.autopilotMinchansize}
validate={this.validateAutopilotChansize}
validateOnBlur
validateOnChange={formState.invalid}
width={100}
ml="auto"
/>
}
/>
<DataRow
py={2}
left={<Label htmlFor="autopilotMaxchansize">Maximum channel size</Label>}
right={
<Input
field="autopilotMaxchansize"
id="autopilotMaxchansize"
variant="thin"
type="number"
min="0"
max="100000000"
step="1"
initialValue={wallet.autopilotMaxchansize}
validate={this.validateAutopilotChansize}
validateOnChange={formState.invalid}
validateOnBlur
width={100}
ml="auto"
/>
}
/>
</React.Fragment>
) : null}
</React.Fragment>
)}
</Form>
)
}
}
export default WalletSettingsFormLocal

39
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 (
<Form
getApi={this.setFormApi}
onSubmit={this.onSubmit}
initialValues={wallet}
wallet={wallet}
{...rest}
>
<Card bg="tertiaryColor" my={3} p={3}>
<pre>{JSON.stringify(wallet, null, 2)}</pre>
</Card>
</Form>
)
}
}
export default WalletSettingsFormRemote

130
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
* <WalletUnlocker
wallet={{ ... }}
unlockWallet={() => {}}
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 (
<Form
width={1}
getApi={this.setFormApi}
onSubmit={this.onSubmit}
onSubmitFailure={this.onSubmitFailure}
key={`wallet-unlocker-form-${wallet.id}`}
>
{({ formState }) => (
<React.Fragment>
<WalletHeader title={walletName} />
<PasswordInput
field="password"
id="password"
label="Enter Password"
my={3}
validate={this.validatePassword}
validateOnBlur
validateOnChange={formState.invalid}
/>
<Button type="submit">Enter</Button>
</React.Fragment>
)}
</Form>
)
}
}
export default withRouter(WalletUnlocker)

60
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 }) => (
<Box {...rest}>
<Text fontWeight="normal">{title}</Text>
{wallets.map(wallet => (
<Text key={wallet.id} py={1}>
<NavLink
to={`/home/wallet/${wallet.id}`}
activeStyle={{ fontWeight: 'normal' }}
onClick={() => setActiveWallet(wallet.id)}
>
{walletName(wallet)}
</NavLink>
</Text>
))}
</Box>
)
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 (
<Box {...rest}>
<WalletGroup
title="Your Wallets"
wallets={localWallets}
setActiveWallet={setActiveWallet}
mb={3}
/>
{otherWallets.length > 0 && (
<WalletGroup title="More" wallets={otherWallets} setActiveWallet={setActiveWallet} />
)}
</Box>
)
}
}
export default WalletsMenu

8
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'

22
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 {
<Wizard.Step
key="SeedView"
component={SeedView}
{...{ seed, generateSeed, fetchingSeed }}
{...{ seed, fetchSeed, fetchingSeed }}
/>,
<Wizard.Step key="SeedConfirm" component={SeedConfirm} {...{ seed }} />,
<Wizard.Step key="Password" component={Password} {...{ setPassword }} />,
@ -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,

8
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,

2
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,

4
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

8
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 (

3
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',

6
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) ||

28
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)

92
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))

44
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))

31
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,

44
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 {
<GlobalError error={error} clearError={clearError} />
<PageWithLoading isLoading={isLoading}>
<Switch>
<Route exact path="/" render={() => <Redirect to="/onboarding" />} />
<Route exact path="/onboarding" component={Onboarding} />
<Route exact path="/syncing" component={Syncing} />
<Route exact path="/app" component={App} />
<Route exact path="/" component={Initializer} />
<Route path="/home" component={Home} />
<Route
exact
path="/onboarding"
render={() => (
<Modal withClose={hasWallets} onClose={() => history.push('/home')}>
<Onboarding />
</Modal>
)}
/>
<Route
exact
path="/syncing"
render={() => (
<Modal withHeader onClose={() => history.push('/logout')} pb={0} px={0}>
<Syncing />
</Modal>
)}
/>
<Route path="/app" component={App} />
<Route path="/logout" component={Logout} />
</Switch>
</PageWithLoading>
</React.Fragment>
@ -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
}

64
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 = <T>(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.

21
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.

4
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

56
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')

4
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
})

28
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

316
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]

361
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: []
}
// ------------------------------------

132
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
}

4
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 = []

112
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 (
<Page css={{ height: 'calc(100vh - 40px)' }}>
<State store={store}>
<Home
startLnd={startLnd}
stopLnd={stopLnd}
unlockWallet={unlockWallet}
deleteWallet={deleteWallet}
setUnlockWalletError={setUnlockWalletError}
setActiveWallet={setActiveWallet}
/>
</State>
</Page>
)
})

4
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}
/>
</State>
)

Loading…
Cancel
Save