Skip to content
Permalink
Browse files

fix(onboarding): prevent duplicate wallet entries

Lock the create/import wallet form submit button whilst the wallet is
in the process of being created to prevent duplicate submissions.

Fix #2087
  • Loading branch information...
mrfelton committed Apr 23, 2019
1 parent 8bf752b commit 8b1b92ee22cfbb8ff60bd67fbd3ace2f6f9733ea
@@ -23,9 +23,9 @@ import messages from './messages'

class Onboarding extends React.Component {
static propTypes = {
// STATE
autopilot: PropTypes.bool, // eslint-disable-line react/boolean-prop-naming
clearStartLndError: PropTypes.func.isRequired,
clearWalletCreateError: PropTypes.func.isRequired,
clearWalletRecoveryError: PropTypes.func.isRequired,
connectionCert: PropTypes.string,
connectionHost: PropTypes.string,
@@ -34,6 +34,7 @@ class Onboarding extends React.Component {
connectionType: PropTypes.string,
createNewWallet: PropTypes.func.isRequired,
fetchSeed: PropTypes.func.isRequired,
isCreatingNewWallet: PropTypes.bool,
isFetchingSeed: PropTypes.bool,
isLightningGrpcActive: PropTypes.bool,
isRecoveringWallet: PropTypes.bool,
@@ -44,8 +45,6 @@ class Onboarding extends React.Component {
passphrase: PropTypes.string,
recoverOldWallet: PropTypes.func.isRequired,
resetOnboarding: PropTypes.func.isRequired,

// DISPATCH
seed: PropTypes.array,
setAutopilot: PropTypes.func.isRequired,
setConnectionCert: PropTypes.func.isRequired,
@@ -70,6 +69,7 @@ class Onboarding extends React.Component {
validateCert: PropTypes.func.isRequired,
validateHost: PropTypes.func.isRequired,
validateMacaroon: PropTypes.func.isRequired,
walletCreateError: PropTypes.string,
walletRecoveryError: PropTypes.string,
}

@@ -109,6 +109,7 @@ class Onboarding extends React.Component {
connectionCert,
connectionMacaroon,
connectionString,
isCreatingNewWallet,
isLightningGrpcActive,
isWalletUnlockerGrpcActive,
passphrase,
@@ -119,11 +120,13 @@ class Onboarding extends React.Component {
unlockWalletError,
isFetchingSeed,
isRecoveringWallet,
walletCreateError,
walletRecoveryError,
lndConnect,

// DISPATCH
clearWalletRecoveryError,
clearWalletCreateError,
setAutopilot,
setConnectionType,
setConnectionHost,
@@ -172,7 +175,16 @@ class Onboarding extends React.Component {
{...{ network, setNetwork, setAutopilot }}
/>,
<Wizard.Step key="Autopilot" component={Autopilot} {...{ autopilot, setAutopilot }} />,
<Wizard.Step key="WalletCreate" component={WalletCreate} {...{ createNewWallet }} />,
<Wizard.Step
key="WalletCreate"
component={WalletCreate}
{...{
clearWalletCreateError,
isCreatingNewWallet,
createNewWallet,
walletCreateError,
}}
/>,
]
break

@@ -4,7 +4,10 @@ import { Form, Spinner, Text } from 'components/UI'

class WalletCreate extends React.Component {
static propTypes = {
clearWalletCreateError: PropTypes.func.isRequired,
createNewWallet: PropTypes.func.isRequired,
isCreatingNewWallet: PropTypes.bool,
walletCreateError: PropTypes.string,
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
}
@@ -15,12 +18,32 @@ class WalletCreate extends React.Component {
}

componentDidMount() {
this.formApi.submitForm()
const { wizardApi } = this.props
wizardApi.next()
}

handleSubmit = () => {
componentDidUpdate(prevProps) {
const { isCreatingNewWallet, walletCreateError } = this.props

// Handle success case.
if (!walletCreateError && !isCreatingNewWallet && prevProps.isCreatingNewWallet) {
this.handleSuccess()
}

// Handle failure case.
if (walletCreateError && !isCreatingNewWallet && prevProps.isCreatingNewWallet) {
this.handleError()
}
}

componentWillUnmount() {
const { clearWalletCreateError } = this.props
clearWalletCreateError()
}

handleSubmit = async () => {
const { createNewWallet } = this.props
createNewWallet()
await createNewWallet()
}

setFormApi = formApi => {
@@ -29,7 +52,7 @@ class WalletCreate extends React.Component {

render() {
const { wizardApi, wizardState, createNewWallet, ...rest } = this.props
const { getApi, onChange, onSubmit, onSubmitFailure } = wizardApi
const { getApi, onChange, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<Form
@@ -41,10 +64,11 @@ class WalletCreate extends React.Component {
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
onSubmit={values => {
this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
onSubmit={async values => {
try {
await this.handleSubmit(values)
} catch (e) {
wizardApi.onSubmitFailure()
}
}}
onSubmitFailure={onSubmitFailure}
@@ -25,7 +25,8 @@ class WalletRecover extends React.Component {
}

componentDidMount() {
this.formApi.submitForm()
const { wizardApi } = this.props
wizardApi.next()
}

componentDidUpdate(prevProps) {
@@ -100,7 +101,13 @@ class WalletRecover extends React.Component {
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
onSubmit={this.handleSubmit}
onSubmit={async values => {
try {
await this.handleSubmit(values)
} catch (e) {
wizardApi.onSubmitFailure()
}
}}
onSubmitFailure={onSubmitFailure}
>
{({ formState }) => {
@@ -29,6 +29,7 @@ import {
startLnd,
stopLnd,
fetchSeed,
clearWalletCreateError,
clearWalletRecoveryError,
createNewWallet,
recoverOldWallet,
@@ -62,6 +63,7 @@ const mapStateToProps = state => ({
})

const mapDispatchToProps = {
clearWalletCreateError,
clearWalletRecoveryError,
setAlias,
setAutopilot,
@@ -26,6 +26,7 @@ export const STOP_LND_SUCCESS = 'STOP_LND_SUCCESS'
export const CREATE_NEW_WALLET = 'CREATE_NEW_WALLET'
export const CREATE_NEW_WALLET_SUCCESS = 'CREATE_NEW_WALLET_SUCCESS'
export const CREATE_NEW_WALLET_FAILURE = 'CREATE_NEW_WALLET_FAILURE'
export const CLEAR_CREATE_NEW_WALLET_ERROR = 'CLEAR_CREATE_NEW_WALLET_ERROR'

export const RECOVER_OLD_WALLET = 'RECOVER_OLD_WALLET'
export const RECOVER_OLD_WALLET_SUCCESS = 'RECOVER_OLD_WALLET_SUCCESS'
@@ -353,6 +354,10 @@ export const fetchSeedError = error => dispatch => {
*/
export const createNewWallet = () => async (dispatch, getState) => {
dispatch({ type: CREATE_NEW_WALLET })

const grpc = await grpcService
let handleLightningActive

try {
const state = getState()
const { chain: defaultChain, network: defaultNetwork } = config
@@ -373,16 +378,39 @@ export const createNewWallet = () => async (dispatch, getState) => {
// Start lnd with the provided wallet config.
await dispatch(startLnd(wallet))

// Set up a listener that resolves once the lightning interface has become active.
// This is the point where we really know that the wallet has been created and we can connect to it.
const waitForLightning = new Promise(resolve => {
handleLightningActive = proxyValue(() => resolve())
grpc.prependListener('GRPC_LIGHTNING_SERVICE_ACTIVE', handleLightningActive)
})

// Call initWallet method.
const grpc = await grpcService
await grpc.services.WalletUnlocker.initWallet({
wallet_password: state.onboarding.password,
cipher_seed_mnemonic: state.onboarding.seed,
recovery_window: 0,
})

// Wait for the lightning gRPC interface to become active.
await waitForLightning

// Notify of wallet recovery success.
dispatch(walletCreated())
} catch (error) {
dispatch(createNewWalletFailure(error.message))
// Attempt to clean up from the failed import attempt.
try {
const { lndConfig } = getState().lnd
await dispatch(stopLnd())
await dispatch(removeWallet(lndConfig))
} finally {
// Remove Lightning grpc activation listener.
if (handleLightningActive) {
grpc.off('GRPC_LIGHTNING_SERVICE_ACTIVE', handleLightningActive)
}
// Notify of wallet recovery failure.
dispatch(createNewWalletFailure(error.message))
}
}
}

@@ -401,6 +429,13 @@ export const createNewWalletFailure = error => ({
error,
})

/**
* Clear wallet create error.
*/
export const clearWalletCreateError = () => ({
type: CLEAR_CREATE_NEW_WALLET_ERROR,
})

/**
* Recover an old wallet.
*/
@@ -583,9 +618,25 @@ const ACTION_HANDLERS = {
isLndActive: false,
}),

[CREATE_NEW_WALLET]: state => ({ ...state, isCreatingNewWallet: true }),
[CREATE_NEW_WALLET_SUCCESS]: state => ({ ...state, isCreatingNewWallet: false }),
[CREATE_NEW_WALLET_FAILURE]: state => ({ ...state, isCreatingNewWallet: false }),
[CREATE_NEW_WALLET]: state => ({
...state,
isCreatingNewWallet: true,
walletCreateError: null,
}),
[CREATE_NEW_WALLET_SUCCESS]: state => ({
...state,
isCreatingNewWallet: false,
walletCreateError: null,
}),
[CREATE_NEW_WALLET_FAILURE]: (state, { error }) => ({
...state,
isCreatingNewWallet: false,
walletCreateError: error,
}),
[CLEAR_CREATE_NEW_WALLET_ERROR]: state => ({
...state,
walletCreateError: null,
}),

[RECOVER_OLD_WALLET]: state => ({
...state,
@@ -1,5 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`reducers neutrinoReducer should handle CLEAR_CREATE_NEW_WALLET_ERROR 1`] = `
Object {
"walletCreateError": null,
}
`;

exports[`reducers neutrinoReducer should handle CLEAR_RECOVER_OLD_WALLET_ERROR 1`] = `
Object {
"walletRecoveryError": null,
@@ -33,12 +39,21 @@ Object {
exports[`reducers neutrinoReducer should handle CREATE_NEW_WALLET 1`] = `
Object {
"isCreatingNewWallet": true,
"walletCreateError": null,
}
`;

exports[`reducers neutrinoReducer should handle CREATE_NEW_WALLET_FAILURE 1`] = `
Object {
"isCreatingNewWallet": false,
"walletCreateError": undefined,
}
`;

exports[`reducers neutrinoReducer should handle CREATE_NEW_WALLET_SUCCESS 1`] = `
Object {
"isCreatingNewWallet": false,
"walletCreateError": null,
}
`;

@@ -7,6 +7,8 @@ import reducer, {
STOP_LND_SUCCESS,
CREATE_NEW_WALLET,
CREATE_NEW_WALLET_SUCCESS,
CREATE_NEW_WALLET_FAILURE,
CLEAR_CREATE_NEW_WALLET_ERROR,
RECOVER_OLD_WALLET,
RECOVER_OLD_WALLET_SUCCESS,
RECOVER_OLD_WALLET_FAILURE,
@@ -99,6 +101,20 @@ describe('reducers', () => {
expect(reducer({}, action)).toMatchSnapshot()
})

it('should handle CREATE_NEW_WALLET_FAILURE', () => {
const action = {
type: CREATE_NEW_WALLET_FAILURE,
}
expect(reducer({}, action)).toMatchSnapshot()
})

it('should handle CLEAR_CREATE_NEW_WALLET_ERROR', () => {
const action = {
type: CLEAR_CREATE_NEW_WALLET_ERROR,
}
expect(reducer({}, action)).toMatchSnapshot()
})

it('should handle RECOVER_OLD_WALLET', () => {
const action = {
type: RECOVER_OLD_WALLET,

0 comments on commit 8b1b92e

Please sign in to comment.
You can’t perform that action at this time.