diff --git a/app/components/Onboarding/Syncing.js b/app/components/Onboarding/Syncing.js index 50c9fc24..0c3b1764 100644 --- a/app/components/Onboarding/Syncing.js +++ b/app/components/Onboarding/Syncing.js @@ -9,23 +9,26 @@ import styles from './Syncing.scss' class Syncing extends Component { componentWillMount() { - const { fetchBlockHeight, blockHeight, newAddress } = this.props + const { fetchBlockHeight, blockHeight } = this.props // If we don't already know the target block height, fetch it now. if (!blockHeight) { fetchBlockHeight() } - - newAddress('np2wkh') } render() { - const { syncPercentage, address, blockHeight, lndBlockHeight } = this.props + const { hasSynced, syncPercentage, address, blockHeight, lndBlockHeight } = this.props + const copyClicked = () => { copy(address) showNotification('Noice', 'Successfully copied to clipboard') } + if (typeof hasSynced === 'undefined') { + return null + } + return (
@@ -35,32 +38,49 @@ class Syncing extends Component { -
-

Fund your Zap wallet

-

Might as well fund your wallet while you're waiting to sync.

-
- - {address.length ? ( -
-
- + {hasSynced === true && ( +
+
+

Welcome back to your Zap wallet!

+

+ Please wait a while whilst we fetch all of your latest data from the blockchain. +

+
+
+
-
- {address} - copy -
- ) : ( -
-
+ )} + + {hasSynced === false && ( +
+
+

Fund your Zap wallet

+

Might as well fund your wallet while you're waiting to sync.

+
+ {address && address.length ? ( +
+
+ +
+
+ {address} + copy +
+
+ ) : ( +
+
+
+ )}
)} @@ -93,8 +113,8 @@ class Syncing extends Component { Syncing.propTypes = { fetchBlockHeight: PropTypes.func.isRequired, - newAddress: PropTypes.func.isRequired, address: PropTypes.string.isRequired, + hasSynced: PropTypes.bool, syncPercentage: PropTypes.number, blockHeight: PropTypes.number, lndBlockHeight: PropTypes.number diff --git a/app/components/Wallet/ReceiveModal.js b/app/components/Wallet/ReceiveModal.js index 323a8a5f..83874282 100644 --- a/app/components/Wallet/ReceiveModal.js +++ b/app/components/Wallet/ReceiveModal.js @@ -117,7 +117,7 @@ ReceiveModal.propTypes = { }).isRequired, isOpen: PropTypes.bool.isRequired, pubkey: PropTypes.string, - address: PropTypes.string.isRequired, + address: PropTypes.string, alias: PropTypes.string, closeReceiveModal: PropTypes.func.isRequired } diff --git a/app/containers/Root.js b/app/containers/Root.js index cf41b704..96b94632 100644 --- a/app/containers/Root.js +++ b/app/containers/Root.js @@ -32,7 +32,7 @@ import { setReEnterSeedIndexes } from '../reducers/onboarding' import { fetchBlockHeight, lndSelectors } from '../reducers/lnd' -import { newAddress } from '../reducers/address' +import { walletAddress } from '../reducers/address' import Routes from '../routes' const mapDispatchToProps = { @@ -54,7 +54,7 @@ const mapDispatchToProps = { unlockWallet, setSignupCreate, setSignupImport, - newAddress, + walletAddress, updateReEnterSeedInput, updateRecoverSeedInput, setReEnterSeedIndexes, @@ -66,6 +66,7 @@ const mapStateToProps = state => ({ lnd: state.lnd, onboarding: state.onboarding, address: state.address, + info: state.info, syncPercentage: lndSelectors.syncPercentage(state), passwordIsValid: onboardingSelectors.passwordIsValid(state), @@ -84,7 +85,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { fetchBlockHeight: dispatchProps.fetchBlockHeight, blockHeight: stateProps.lnd.blockHeight, lndBlockHeight: stateProps.lnd.lndBlockHeight, - newAddress: dispatchProps.newAddress, + hasSynced: stateProps.info.hasSynced, syncPercentage: stateProps.syncPercentage, address: stateProps.address.address } diff --git a/app/lnd/methods/index.js b/app/lnd/methods/index.js index 4e081765..df66368d 100644 --- a/app/lnd/methods/index.js +++ b/app/lnd/methods/index.js @@ -60,7 +60,7 @@ export default function(lnd, log, event, msg, data) { // Data looks like { address: '' } walletController .newAddress(lnd, data.type) - .then(({ address }) => event.sender.send('receiveAddress', address)) + .then(({ address }) => event.sender.send('receiveAddress', { type: data.type, address })) .catch(error => log.error('newaddress:', error)) break case 'setAlias': diff --git a/app/reducers/address.js b/app/reducers/address.js index 29af7b97..209a708f 100644 --- a/app/reducers/address.js +++ b/app/reducers/address.js @@ -1,4 +1,6 @@ import { ipcRenderer } from 'electron' +import Store from 'electron-store' + // ------------------------------------ // Constants // ------------------------------------ @@ -36,15 +38,46 @@ export function closeWalletModal() { } } +// Get our existing address if there is one, otherwise generate a new one. +export const walletAddress = type => (dispatch, getState) => { + let address + + // Wallet addresses are keyed under the node pubKey in our store. + const state = getState() + const pubKey = state.info.data.identity_pubkey + if (pubKey) { + const store = new Store({ name: 'wallet' }) + address = store.get(`${pubKey}.${type}`, null) + } + + // If we have an address already, use that. Otherwise, generate a new address. + if (address) { + dispatch({ type: RECEIVE_ADDRESS, address }) + } else { + dispatch(newAddress(type)) + } +} + // Send IPC event for getinfo -export const newAddress = type => async dispatch => { +export const newAddress = type => dispatch => { dispatch(getAddress()) ipcRenderer.send('lnd', { msg: 'newaddress', data: { type: addressTypes[type] } }) } // Receive IPC event for info -export const receiveAddress = (event, address) => dispatch => - dispatch({ type: RECEIVE_ADDRESS, address }) +export const receiveAddress = (event, data) => (dispatch, getState) => { + const state = getState() + const pubKey = state.info.data.identity_pubkey + + // If we know the node's public key, store the address for reuse. + if (pubKey) { + const type = Object.keys(addressTypes).find(key => addressTypes[key] === data.type) + const store = new Store({ name: 'wallet' }) + store.set(`${pubKey}.${type}`, data.address) + } + + dispatch({ type: RECEIVE_ADDRESS, address: data.address }) +} // ------------------------------------ // Action Handlers diff --git a/app/reducers/info.js b/app/reducers/info.js index 49dba465..a7304242 100644 --- a/app/reducers/info.js +++ b/app/reducers/info.js @@ -1,6 +1,8 @@ +import Store from 'electron-store' import bitcoin from 'bitcoinjs-lib' import { ipcRenderer } from 'electron' +import { walletAddress } from './address' // ------------------------------------ // Constants @@ -8,6 +10,7 @@ import { ipcRenderer } from 'electron' export const GET_INFO = 'GET_INFO' export const RECEIVE_INFO = 'RECEIVE_INFO' export const SET_WALLET_CURRENCY_FILTERS = 'SET_WALLET_CURRENCY_FILTERS' +export const SET_HAS_SYNCED = 'SET_HAS_SYNCED' // ------------------------------------ // Actions @@ -25,6 +28,13 @@ export function setWalletCurrencyFilters(showWalletCurrencyFilters) { } } +export const setHasSynced = hasSynced => { + return { + type: SET_HAS_SYNCED, + hasSynced + } +} + // Send IPC event for getinfo export const fetchInfo = () => async dispatch => { dispatch(getInfo()) @@ -32,8 +42,20 @@ export const fetchInfo = () => async dispatch => { } // Receive IPC event for info -export const receiveInfo = (event, data) => dispatch => { +export const receiveInfo = (event, data) => (dispatch, getState) => { dispatch({ type: RECEIVE_INFO, data }) + + // Now that we have the node info, get the current wallet address. + dispatch(walletAddress('np2wkh')) + + // Determine the node's current sync state. + const state = getState() + if (typeof state.info.hasSynced === 'undefined') { + const store = new Store({ name: 'wallet' }) + const hasSynced = store.get(`${data.identity_pubkey}.hasSynced`, false) + store.set(`${data.identity_pubkey}.hasSynced`, hasSynced) + dispatch(setHasSynced(hasSynced)) + } } const networks = { @@ -57,6 +79,10 @@ const networks = { // Action Handlers // ------------------------------------ const ACTION_HANDLERS = { + [SET_HAS_SYNCED]: (state, { hasSynced }) => ({ + ...state, + hasSynced + }), [GET_INFO]: state => ({ ...state, infoLoading: true }), [RECEIVE_INFO]: (state, { data }) => ({ ...state, @@ -75,6 +101,7 @@ const ACTION_HANDLERS = { // ------------------------------------ const initialState = { infoLoading: false, + hasSynced: undefined, network: {}, data: {}, showWalletCurrencyFilters: false diff --git a/app/reducers/lnd.js b/app/reducers/lnd.js index 2bb63f3a..9b2263d4 100644 --- a/app/reducers/lnd.js +++ b/app/reducers/lnd.js @@ -1,7 +1,8 @@ +import Store from 'electron-store' import { createSelector } from 'reselect' import { fetchTicker } from './ticker' import { fetchBalance } from './balance' -import { fetchInfo } from './info' +import { fetchInfo, setHasSynced } from './info' import { requestBlockHeight } from '../api' import { showNotification } from '../notifications' // ------------------------------------ @@ -25,14 +26,23 @@ export const GRPC_CONNECTED = 'GRPC_CONNECTED' export const lndSyncing = () => dispatch => dispatch({ type: START_SYNCING }) // Receive IPC event for LND stoping sync -export const lndSynced = () => dispatch => { +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 + if (pubKey) { + const store = new Store({ name: 'wallet' }) + 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()) - dispatch({ type: STOP_SYNCING }) - // HTML 5 desktop notification for the new transaction const notifTitle = 'Lightning Node Synced' const notifBody = "Visa who? You're your own payment processor now!" diff --git a/app/reducers/transaction.js b/app/reducers/transaction.js index 42798639..69dfb4c3 100644 --- a/app/reducers/transaction.js +++ b/app/reducers/transaction.js @@ -58,9 +58,21 @@ export const fetchTransactions = () => dispatch => { } // Receive IPC event for payments -export const receiveTransactions = (event, { transactions }) => dispatch => +export const receiveTransactions = (event, { transactions }) => (dispatch, getState) => { dispatch({ type: RECEIVE_TRANSACTIONS, transactions }) + // If our current wallet address has been used, generate a new one. + const state = getState() + const currentAddress = state.address.address + let usedAddresses = [] + transactions.forEach(transaction => { + usedAddresses = usedAddresses.concat(transaction.dest_addresses) + }) + if (usedAddresses.includes(currentAddress)) { + dispatch(newAddress('np2wkh')) + } +} + export const sendCoins = ({ value, addr, currency }) => dispatch => { // backend needs amount in satoshis no matter what currency we are using const amount = btc.convert(currency, 'sats', value) diff --git a/app/routes/activity/containers/ActivityContainer.js b/app/routes/activity/containers/ActivityContainer.js index 1ed441e2..9a30bfd9 100644 --- a/app/routes/activity/containers/ActivityContainer.js +++ b/app/routes/activity/containers/ActivityContainer.js @@ -13,7 +13,7 @@ import { updateSearchActive, updateSearchText } from 'reducers/activity' -import { newAddress, openWalletModal } from 'reducers/address' +import { walletAddress, openWalletModal } from 'reducers/address' import { setFormType } from 'reducers/form' import { payFormSelectors } from 'reducers/payform' @@ -33,7 +33,7 @@ const mapDispatchToProps = { hideActivityModal, changeFilter, toggleFilterPulldown, - newAddress, + walletAddress, openWalletModal, fetchBalance, updateSearchActive, @@ -93,7 +93,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => ({ setCurrency: dispatchProps.setCurrency, setWalletCurrencyFilters: dispatchProps.setWalletCurrencyFilters, - newAddress: dispatchProps.newAddress, + walletAddress: dispatchProps.walletAddress, openReceiveModal: dispatchProps.openWalletModal, openPayForm: () => dispatchProps.setFormType('PAY_FORM'), openRequestForm: () => dispatchProps.setFormType('REQUEST_FORM') diff --git a/app/routes/app/components/App.js b/app/routes/app/components/App.js index b48d0e43..53354203 100644 --- a/app/routes/app/components/App.js +++ b/app/routes/app/components/App.js @@ -21,7 +21,6 @@ class App extends Component { const { fetchTicker, fetchInfo, - newAddress, fetchChannels, fetchSuggestedNodes, fetchBalance, @@ -32,8 +31,6 @@ class App extends Component { fetchTicker() // fetch node info fetchInfo() - // fetch new address for wallet - newAddress('np2wkh') // fetch nodes channels fetchChannels() // fetch suggested nodes list from zap.jackmallers.com/suggested-peers @@ -108,7 +105,6 @@ App.propTypes = { receiveModalProps: PropTypes.object, channelFormProps: PropTypes.object, - newAddress: PropTypes.func.isRequired, fetchInfo: PropTypes.func.isRequired, fetchTicker: PropTypes.func.isRequired, clearError: PropTypes.func.isRequired, diff --git a/app/routes/app/containers/AppContainer.js b/app/routes/app/containers/AppContainer.js index 98c5ee67..aa2b1016 100644 --- a/app/routes/app/containers/AppContainer.js +++ b/app/routes/app/containers/AppContainer.js @@ -5,7 +5,7 @@ import { btc } from 'utils' import { fetchTicker, setCurrency, tickerSelectors } from 'reducers/ticker' -import { newAddress, closeWalletModal } from 'reducers/address' +import { closeWalletModal } from 'reducers/address' import { fetchInfo } from 'reducers/info' @@ -79,7 +79,6 @@ const mapDispatchToProps = { fetchTicker, setCurrency, - newAddress, closeWalletModal, fetchInfo, @@ -396,7 +395,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { pubkey: stateProps.info.data.identity_pubkey, address: stateProps.address.address, alias: stateProps.info.data.alias, - newAddress: dispatchProps.newAddress, closeReceiveModal: dispatchProps.closeWalletModal } diff --git a/test/reducers/__snapshots__/info.spec.js.snap b/test/reducers/__snapshots__/info.spec.js.snap index e73a2362..ceb3f788 100644 --- a/test/reducers/__snapshots__/info.spec.js.snap +++ b/test/reducers/__snapshots__/info.spec.js.snap @@ -3,6 +3,7 @@ exports[`reducers infoReducer should correctly getInfo 1`] = ` Object { "data": Object {}, + "hasSynced": undefined, "infoLoading": true, "network": Object {}, "showWalletCurrencyFilters": false, @@ -12,6 +13,7 @@ Object { exports[`reducers infoReducer should correctly receiveInfo 1`] = ` Object { "data": "foo", + "hasSynced": undefined, "infoLoading": false, "network": Object { "bitcoinJsNetwork": Object { @@ -37,6 +39,7 @@ Object { exports[`reducers infoReducer should handle initial state 1`] = ` Object { "data": Object {}, + "hasSynced": undefined, "infoLoading": false, "network": Object {}, "showWalletCurrencyFilters": false,