diff --git a/app/components/Onboarding/Syncing.js b/app/components/Onboarding/Syncing.js index c8932b67..27b76d71 100644 --- a/app/components/Onboarding/Syncing.js +++ b/app/components/Onboarding/Syncing.js @@ -8,15 +8,60 @@ import { showNotification } from 'notifications' import styles from './Syncing.scss' class Syncing extends Component { - componentWillMount() {} + state = { + timer: null, + syncMessageDetail: null + } + + componentWillMount() { + const { syncStatus } = this.props + + // If we are still waiting for peers after some time, advise te user it could take a wile. + let timer = setTimeout(() => { + if (syncStatus === 'waiting') { + this.setState({ + syncMessageDetail: + 'It looks like this could take some time - you might want to grab a coffee or try again later!' + }) + } + }, 10000) + + this.setState({ timer }) + } + + componentWillUnmount() { + const { timer } = this.state + clearInterval(timer) + } render() { - const { hasSynced, syncPercentage, address, blockHeight, lndBlockHeight } = this.props + const { + hasSynced, + syncStatus, + syncPercentage, + address, + blockHeight, + lndBlockHeight + } = this.props + let { syncMessageDetail } = this.state const copyClicked = () => { copy(address) showNotification('Noice', 'Successfully copied to clipboard') } + let syncMessage + if (syncStatus === 'waiting') { + syncMessage = 'Waiting for peers...' + } else if (typeof syncPercentage === 'undefined' || syncPercentage <= 0) { + syncMessage = 'Preparing...' + syncMessageDetail = null + } else if (syncPercentage > 0 && syncPercentage < 99) { + syncMessage = `${syncPercentage}%` + syncMessageDetail = `${lndBlockHeight.toLocaleString()} of ${blockHeight.toLocaleString()}` + } else if (syncPercentage >= 99) { + syncMessage = 'Finalizing...' + syncMessageDetail = null + } if (typeof hasSynced === 'undefined') { return null @@ -78,24 +123,16 @@ class Syncing extends Component { )}
-

Syncing to the blockchain...

+

Syncing to the blockchain

-

- {typeof syncPercentage === 'undefined' && 'Preparing...'} - {Boolean(syncPercentage >= 0 && syncPercentage < 99) && `${syncPercentage}%`} - {Boolean(syncPercentage >= 99) && 'Finalizing...'} -

- {Boolean(syncPercentage >= 0 && syncPercentage < 99) && ( - - {Boolean(!blockHeight || !lndBlockHeight) && 'starting...'} - {Boolean(blockHeight && lndBlockHeight) && - `${lndBlockHeight.toLocaleString()} of ${blockHeight.toLocaleString()}`} - +

{syncMessage}

+ {syncMessageDetail && ( + {syncMessageDetail} )}
@@ -107,6 +144,7 @@ class Syncing extends Component { Syncing.propTypes = { address: PropTypes.string.isRequired, hasSynced: PropTypes.bool, + syncStatus: PropTypes.string.isRequired, syncPercentage: PropTypes.number, blockHeight: PropTypes.number, lndBlockHeight: PropTypes.number diff --git a/app/components/Onboarding/Syncing.scss b/app/components/Onboarding/Syncing.scss index 9f0513b2..3ffd9808 100644 --- a/app/components/Onboarding/Syncing.scss +++ b/app/components/Onboarding/Syncing.scss @@ -95,7 +95,7 @@ margin-top: 10px; } - .progressCounter { + .progressDetail { color: $gold; font-size: 12px; margin-top: 10px; diff --git a/app/containers/Root.js b/app/containers/Root.js index b64a26b1..e6bd7677 100644 --- a/app/containers/Root.js +++ b/app/containers/Root.js @@ -81,6 +81,7 @@ const mapStateToProps = state => ({ const mergeProps = (stateProps, dispatchProps, ownProps) => { const syncingProps = { blockHeight: stateProps.lnd.blockHeight, + syncStatus: stateProps.lnd.syncStatus, lndBlockHeight: stateProps.lnd.lndBlockHeight, hasSynced: stateProps.info.hasSynced, syncPercentage: stateProps.syncPercentage, @@ -213,7 +214,11 @@ const Root = ({ } // If we are syncing show the syncing screen - if (lnd.grpcStarted && lnd.syncing) { + if ( + onboardingProps.onboarding.connectionType === 'local' && + lnd.grpcStarted && + lnd.syncStatus !== 'complete' + ) { return } diff --git a/app/lnd/lib/neutrino.js b/app/lnd/lib/neutrino.js index b6b6fe61..0685a877 100644 --- a/app/lnd/lib/neutrino.js +++ b/app/lnd/lib/neutrino.js @@ -5,20 +5,37 @@ import config from '../config' import { mainLog, lndLog, lndLogGetLevel } from '../../utils/log' import { fetchBlockHeight } from './util' +// Sync status is currenty pending. +const NEUTRINO_SYNC_STATUS_PENDING = 'chain-sync-pending' + +// Waiting for chain backend to finish synchronizing. +const NEUTRINO_SYNC_STATUS_WAITING = 'chain-sync-waiting' + +// Initial sync is currently in progress. +const NEUTRINO_SYNC_STATUS_IN_PROGRESS = 'chain-sync-started' + +// Initial sync has completed. +const NEUTRINO_SYNC_STATUS_COMPLETE = 'chain-sync-finished' + +/** + * Wrapper class for Lnd to run and monitor it in Neutrino mode. + * @extends EventEmitter + */ class Neutrino extends EventEmitter { constructor(alias, autopilot) { super() this.alias = alias this.autopilot = autopilot this.process = null - this.state = { - grpcProxyStarted: false, - walletOpened: false, - chainSyncStarted: false, - chainSyncFinished: false - } + this.grpcProxyStarted = false + this.walletOpened = false + this.chainSyncStatus = NEUTRINO_SYNC_STATUS_PENDING } + /** + * Start the Lnd process in Neutrino mode. + * @return {Number} PID of the Lnd process that was started. + */ start() { if (this.process) { throw new Error('Neutrino process with PID ${this.process.pid} already exists.') @@ -59,28 +76,43 @@ class Neutrino extends EventEmitter { } // gRPC started. - if (!this.state.grpcProxyStarted) { + if (!this.grpcProxyStarted) { if (line.includes('gRPC proxy started') && line.includes('password')) { - this.state.grpcProxyStarted = true + this.grpcProxyStarted = true this.emit('grpc-proxy-started') } } // Wallet opened. - if (!this.state.walletOpened) { + if (!this.walletOpened) { if (line.includes('gRPC proxy started') && !line.includes('password')) { - this.state.walletOpened = true + this.walletOpened = true this.emit('wallet-opened') } } - // LND syncing has started. - if (!this.state.chainSyncStarted) { + // If the sync has already completed then we don't need to do anythibng else. + if (this.is(NEUTRINO_SYNC_STATUS_COMPLETE)) { + return + } + + // Lnd waiting for backend to finish syncing. + if (this.is(NEUTRINO_SYNC_STATUS_PENDING) || this.is(NEUTRINO_SYNC_STATUS_IN_PROGRESS)) { + if ( + line.includes('No sync peer candidates available') || + line.includes('Unable to synchronize wallet to chain') || + line.includes('Waiting for chain backend to finish sync') + ) { + this.setState(NEUTRINO_SYNC_STATUS_WAITING) + } + } + + // Lnd syncing has started or resumed. + if (this.is(NEUTRINO_SYNC_STATUS_PENDING) || this.is(NEUTRINO_SYNC_STATUS_WAITING)) { const match = line.match(/Syncing to block height (\d+)/) if (match) { // Notify that chhain syncronisation has now started. - this.state.chainSyncStarted = true - this.emit('chain-sync-started') + this.setState(NEUTRINO_SYNC_STATUS_IN_PROGRESS) // This is the latest block that BTCd is aware of. const btcdHeight = Number(match[1]) @@ -98,25 +130,27 @@ class Neutrino extends EventEmitter { } } - // LND syncing has completed. - if (!this.state.chainSyncFinished) { - if (line.includes('Chain backend is fully synced')) { - this.state.chainSyncFinished = true - this.emit('chain-sync-finished') - } - } - - // Pass current block height progress to front end for loading state UX - if (this.state.chainSyncStarted) { + // Lnd as received some updated block data. + if (this.is(NEUTRINO_SYNC_STATUS_WAITING) || this.is(NEUTRINO_SYNC_STATUS_IN_PROGRESS)) { + let height let match - if ((match = line.match(/Downloading headers for blocks (\d+) to \d+/))) { - this.emit('got-lnd-block-height', match[1]) - } else if ((match = line.match(/Rescanned through block.+\(height (\d+)/))) { - this.emit('got-lnd-block-height', match[1]) + + if ((match = line.match(/Rescanned through block.+\(height (\d+)/))) { + height = match[1] } else if ((match = line.match(/Caught up to height (\d+)/))) { - this.emit('got-lnd-block-height', match[1]) + height = match[1] } else if ((match = line.match(/Processed \d* blocks? in the last.+\(height (\d+)/))) { - this.emit('got-lnd-block-height', match[1]) + height = match[1] + } + + if (height) { + this.setState(NEUTRINO_SYNC_STATUS_IN_PROGRESS) + this.emit('got-lnd-block-height', height) + } + + // Lnd syncing has completed. + if (line.includes('Chain backend is fully synced')) { + this.setState(NEUTRINO_SYNC_STATUS_COMPLETE) } } }) @@ -124,12 +158,35 @@ class Neutrino extends EventEmitter { return this.process } + /** + * Stop the Lnd process. + */ stop() { if (this.process) { this.process.kill() this.process = null } } + + /** + * Check if the current state matches the passted in state. + * @param {String} state State to compare against the current state. + * @return {Boolean} Boolean indicating if the current state matches the passed in state. + */ + is(state) { + return this.chainSyncStatus === state + } + + /** + * Set the current state and emit an event to notify others if te state as canged. + * @param {String} state Target state. + */ + setState(state) { + if (state !== this.chainSyncStatus) { + this.chainSyncStatus = state + this.emit(state) + } + } } export default Neutrino diff --git a/app/reducers/ipc.js b/app/reducers/ipc.js index e8064367..beabeeb9 100644 --- a/app/reducers/ipc.js +++ b/app/reducers/ipc.js @@ -1,7 +1,6 @@ import createIpc from 'redux-electron-ipc' import { - lndSyncing, - lndSynced, + lndSyncStatus, currentBlockHeight, lndBlockHeight, grpcDisconnected, @@ -58,8 +57,7 @@ import { // Import all receiving IPC event handlers and pass them into createIpc const ipc = createIpc({ - lndSyncing, - lndSynced, + lndSyncStatus, currentBlockHeight, lndBlockHeight, grpcDisconnected, diff --git a/app/reducers/lnd.js b/app/reducers/lnd.js index c90d241b..ab3efef8 100644 --- a/app/reducers/lnd.js +++ b/app/reducers/lnd.js @@ -7,10 +7,11 @@ import { showNotification } from '../notifications' // ------------------------------------ // Constants // ------------------------------------ -export const START_SYNCING = 'START_SYNCING' -export const STOP_SYNCING = 'STOP_SYNCING' +export const SET_SYNC_STATUS_PENDING = 'SET_SYNC_STATUS_PENDING' +export const SET_SYNC_STATUS_WAITING = 'SET_SYNC_STATUS_WAITING' +export const SET_SYNC_STATUS_IN_PROGRESS = 'SET_SYNC_STATUS_IN_PROGRESS' +export const SET_SYNC_STATUS_COMPLETE = 'SET_SYNC_STATUS_COMPLETE' -export const GET_BLOCK_HEIGHT = 'GET_BLOCK_HEIGHT' export const RECEIVE_BLOCK_HEIGHT = 'RECEIVE_BLOCK_HEIGHT' export const RECEIVE_BLOCK = 'RECEIVE_BLOCK' @@ -21,11 +22,11 @@ export const GRPC_CONNECTED = 'GRPC_CONNECTED' // Actions // ------------------------------------ -// Receive IPC event for LND starting its syncing process -export const lndSyncing = () => dispatch => dispatch({ type: START_SYNCING }) +// Receive IPC event for LND sync status change. +export const lndSyncStatus = (event, status) => (dispatch, getState) => { + const notifTitle = 'Lightning Node Synced' + const notifBody = "Visa who? You're your own payment processor now!" -// Receive IPC event for LND stoping sync -export const lndSynced = () => (dispatch, getState) => { // Persist the fact that the wallet has been synced at least once. const state = getState() const pubKey = state.info.data.identity_pubkey @@ -34,19 +35,29 @@ export const lndSynced = () => (dispatch, getState) => { store.set(`${pubKey}.hasSynced`, true) } - dispatch({ type: STOP_SYNCING }) - dispatch(setHasSynced(true)) - - // Fetch data now that we know LND is synced - dispatch(fetchTicker()) - dispatch(fetchBalance()) - dispatch(fetchInfo()) - - // HTML 5 desktop notification for the new transaction - const notifTitle = 'Lightning Node Synced' - const notifBody = "Visa who? You're your own payment processor now!" - - showNotification(notifTitle, notifBody) + switch (status) { + case 'waiting': + dispatch({ type: SET_SYNC_STATUS_WAITING }) + break + case 'in-progress': + dispatch({ type: SET_SYNC_STATUS_IN_PROGRESS }) + break + case 'complete': + dispatch({ type: SET_SYNC_STATUS_COMPLETE }) + + dispatch(setHasSynced(true)) + + // Fetch data now that we know LND is synced + dispatch(fetchTicker()) + dispatch(fetchBalance()) + dispatch(fetchInfo()) + + // HTML 5 desktop notification for the new transaction + showNotification(notifTitle, notifBody) + break + default: + dispatch({ type: SET_SYNC_STATUS_PENDING }) + } } export const grpcDisconnected = () => dispatch => dispatch({ type: GRPC_DISCONNECTED }) @@ -76,8 +87,10 @@ export function receiveBlockHeight(blockHeight) { // Action Handlers // ------------------------------------ const ACTION_HANDLERS = { - [START_SYNCING]: state => ({ ...state, syncing: true }), - [STOP_SYNCING]: state => ({ ...state, syncing: false }), + [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' }), + [SET_SYNC_STATUS_COMPLETE]: state => ({ ...state, syncStatus: 'complete' }), [RECEIVE_BLOCK_HEIGHT]: (state, { blockHeight }) => ({ ...state, @@ -93,9 +106,8 @@ const ACTION_HANDLERS = { // Reducer // ------------------------------------ const initialState = { - syncing: false, + syncStatus: 'pending', grpcStarted: false, - lines: [], blockHeight: 0, lndBlockHeight: 0 } diff --git a/app/zap.js b/app/zap.js index e2172db4..8e9abc77 100644 --- a/app/zap.js +++ b/app/zap.js @@ -160,14 +160,19 @@ class ZapController { this.startGrpc() }) + this.neutrino.on('chain-sync-waiting', () => { + mainLog.info('Neutrino sync waiting') + this.sendMessage('lndSyncStatus', 'waiting') + }) + this.neutrino.on('chain-sync-started', () => { mainLog.info('Neutrino sync started') - this.sendMessage('lndSyncing') + this.sendMessage('lndSyncStatus', 'in-progress') }) this.neutrino.on('chain-sync-finished', () => { mainLog.info('Neutrino sync finished') - this.sendMessage('lndSynced') + this.sendMessage('lndSyncStatus', 'complete') }) this.neutrino.on('got-current-block-height', height => {