Browse Source

feat(sync): handle btcd node still syncing

Handle the case were the Neutrino backend BTCd node is still
synchronising the blockchain as LND is not able to start syncing
until the BTCd node that it is connected to is fully synced.
renovate/lint-staged-8.x
Tom Kirkpatrick 7 years ago
parent
commit
637b27d97d
No known key found for this signature in database GPG Key ID: 72203A8EC5967EA8
  1. 66
      app/components/Onboarding/Syncing.js
  2. 2
      app/components/Onboarding/Syncing.scss
  3. 7
      app/containers/Root.js
  4. 117
      app/lnd/lib/neutrino.js
  5. 6
      app/reducers/ipc.js
  6. 60
      app/reducers/lnd.js
  7. 9
      app/zap.js

66
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 {
)}
<section className={styles.progressContainer}>
<h3>Syncing to the blockchain...</h3>
<h3>Syncing to the blockchain</h3>
<div className={styles.progressBar}>
<div
className={styles.progress}
style={{ width: syncPercentage ? `${syncPercentage}%` : 0 }}
/>
</div>
<h4>
{typeof syncPercentage === 'undefined' && 'Preparing...'}
{Boolean(syncPercentage >= 0 && syncPercentage < 99) && `${syncPercentage}%`}
{Boolean(syncPercentage >= 99) && 'Finalizing...'}
</h4>
{Boolean(syncPercentage >= 0 && syncPercentage < 99) && (
<span className={styles.progressCounter}>
{Boolean(!blockHeight || !lndBlockHeight) && 'starting...'}
{Boolean(blockHeight && lndBlockHeight) &&
`${lndBlockHeight.toLocaleString()} of ${blockHeight.toLocaleString()}`}
</span>
<h4>{syncMessage}</h4>
{syncMessageDetail && (
<span className={styles.progressDetail}>{syncMessageDetail}</span>
)}
</section>
</div>
@ -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

2
app/components/Onboarding/Syncing.scss

@ -95,7 +95,7 @@
margin-top: 10px;
}
.progressCounter {
.progressDetail {
color: $gold;
font-size: 12px;
margin-top: 10px;

7
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 <Syncing {...syncingProps} />
}

117
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

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

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

9
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 => {

Loading…
Cancel
Save