diff --git a/src/actions/account.js b/src/actions/account.js index 30ad88c9c4..66572099d7 100644 --- a/src/actions/account.js +++ b/src/actions/account.js @@ -2,9 +2,10 @@ import i18next from 'i18next'; import actionTypes from '../constants/actions'; import { setSecondPassphrase, getAccount } from '../utils/api/account'; import { registerDelegate, getDelegate, getVotes, getVoters } from '../utils/api/delegate'; -import { loadTransactionsFinish } from './transactions'; +import { loadTransactionsFinish, transactionsUpdated } from './transactions'; import { delegateRegisteredFailure } from './delegate'; import { errorAlertDialogDisplayed } from './dialog'; +import { activePeerUpdate } from './peers'; import Fees from '../constants/fees'; import transactionTypes from '../constants/transactionTypes'; @@ -159,7 +160,6 @@ export const loadAccount = ({ transactionsResponse, isSameAccount, }) => - (dispatch) => { getAccount(activePeer, address) .then((response) => { @@ -184,3 +184,46 @@ export const loadAccount = ({ dispatch(loadTransactionsFinish(accountDataUpdated)); }); }; + +export const updateTransactionsIfNeeded = ({ transactions, activePeer, account }, windowFocus) => + (dispatch) => { + const hasRecentTransactions = txs => ( + txs.confirmed.filter(tx => tx.confirmations < 1000).length !== 0 || + txs.pending.length !== 0 + ); + + if (windowFocus || !hasRecentTransactions(transactions)) { + const { filter } = transactions; + const address = transactions.account ? transactions.account.address : account.address; + + dispatch(transactionsUpdated({ + pendingTransactions: transactions.pending, + activePeer, + address, + limit: 25, + filter, + })); + } + }; + +export const accountDataUpdated = ({ + peers, account, windowIsFocused, transactions, +}) => + (dispatch) => { + getAccount(peers.data, account.address).then((result) => { + if (result.balance !== account.balance) { + dispatch(updateTransactionsIfNeeded( + { + transactions, + activePeer: peers.data, + account, + }, + !windowIsFocused, + )); + } + dispatch(accountUpdated(result)); + dispatch(activePeerUpdate({ online: true })); + }).catch((res) => { + dispatch(activePeerUpdate({ online: false, code: res.error.code })); + }); + }; diff --git a/src/actions/account.test.js b/src/actions/account.test.js index 9b5516fcfa..1e784dbd34 100644 --- a/src/actions/account.test.js +++ b/src/actions/account.test.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import sinon from 'sinon'; +import { spy, stub } from 'sinon'; import actionTypes from '../constants/actions'; import { accountUpdated, @@ -9,6 +9,9 @@ import { removePassphrase, passphraseUsed, loadDelegate, + loadAccount, + accountDataUpdated, + updateTransactionsIfNeeded, } from './account'; import { errorAlertDialogDisplayed } from './dialog'; import { delegateRegisteredFailure } from './delegate'; @@ -18,6 +21,8 @@ import Fees from '../constants/fees'; import transactionTypes from '../constants/transactionTypes'; import networks from '../constants/networks'; import accounts from '../../test/constants/accounts'; +import * as peersActions from './peers'; +import * as transactionsActions from './transactions'; describe('actions: account', () => { describe('accountUpdated', () => { @@ -58,8 +63,8 @@ describe('actions: account', () => { let dispatch; beforeEach(() => { - accountApiMock = sinon.stub(accountApi, 'setSecondPassphrase'); - dispatch = sinon.spy(); + accountApiMock = stub(accountApi, 'setSecondPassphrase'); + dispatch = spy(); }); afterEach(() => { @@ -119,8 +124,8 @@ describe('actions: account', () => { let dispatch; beforeEach(() => { - delegateApiMock = sinon.stub(delegateApi, 'registerDelegate'); - dispatch = sinon.spy(); + delegateApiMock = stub(delegateApi, 'registerDelegate'); + dispatch = spy(); }); afterEach(() => { @@ -175,8 +180,8 @@ describe('actions: account', () => { const actionFunction = loadDelegate(data); beforeEach(() => { - delegateApiMock = sinon.stub(delegateApi, 'getDelegate'); - dispatch = sinon.spy(); + delegateApiMock = stub(delegateApi, 'getDelegate'); + dispatch = spy(); }); afterEach(() => { @@ -212,4 +217,164 @@ describe('actions: account', () => { expect(removePassphrase(data)).to.be.deep.equal(expectedAction); }); }); + + describe('loadAccount', () => { + let getAccountStub; + let transactionsActionsStub; + + const dispatch = spy(); + + beforeEach(() => { + getAccountStub = stub(accountApi, 'getAccount').returnsPromise(); + transactionsActionsStub = spy(transactionsActions, 'loadTransactionsFinish'); + }); + + afterEach(() => { + getAccountStub.restore(); + transactionsActionsStub.restore(); + }); + + it('should finish transactions load and load delegate if not own account', () => { + getAccountStub.resolves({ + balance: 10e8, + publicKey: accounts.genesis.publicKey, + isDelegate: false, + }); + + const data = { + activePeer: {}, + address: accounts.genesis.address, + transactionsResponse: { count: 0, transactions: [] }, + isSameAccount: false, + }; + + loadAccount(data)(dispatch); + expect(transactionsActionsStub).to.have.been.calledWith({ + confirmed: [], + count: 0, + balance: 10e8, + address: accounts.genesis.address, + }); + }); + + it('should finish transactions load and should not load delegate if own account', () => { + getAccountStub.resolves({ + balance: 10e8, + publicKey: accounts.genesis.publicKey, + isDelegate: true, + delegate: 'delegate information', + }); + + const data = { + activePeer: {}, + address: accounts.genesis.address, + transactionsResponse: { count: 0, transactions: [] }, + isSameAccount: true, + }; + + loadAccount(data)(dispatch); + expect(transactionsActionsStub).to.have.been.calledWith({ + confirmed: [], + count: 0, + balance: 10e8, + address: accounts.genesis.address, + delegate: 'delegate information', + }); + }); + }); + + describe('accountDataUpdated', () => { + let peersActionsStub; + let getAccountStub; + let transactionsActionsStub; + + const dispatch = spy(); + + beforeEach(() => { + peersActionsStub = spy(peersActions, 'activePeerUpdate'); + getAccountStub = stub(accountApi, 'getAccount').returnsPromise(); + transactionsActionsStub = spy(transactionsActions, 'transactionsUpdated'); + }); + + afterEach(() => { + getAccountStub.restore(); + peersActionsStub.restore(); + transactionsActionsStub.restore(); + }); + + it(`should call account API methods on ${actionTypes.newBlockCreated} action when online`, () => { + getAccountStub.resolves({ balance: 10e8 }); + + const data = { + windowIsFocused: false, + peers: { data: {} }, + transactions: { + pending: [{ + id: 12498250891724098, + }], + confirmed: [], + account: { address: 'test_address', balance: 0 }, + }, + account: { address: accounts.genesis.address, balance: 0 }, + }; + + accountDataUpdated(data)(dispatch); + expect(dispatch).to.have.callCount(3); + expect(peersActionsStub).to.have.not.been.calledWith({ online: false, code: 'EUNAVAILABLE' }); + }); + + it(`should call account API methods on ${actionTypes.newBlockCreated} action when offline`, () => { + getAccountStub.rejects({ error: { code: 'EUNAVAILABLE' } }); + + const data = { + windowIsFocused: true, + peers: { data: {} }, + transactions: { + pending: [{ id: 12498250891724098 }], + confirmed: [], + account: { address: 'test_address', balance: 0 }, + }, + account: { address: accounts.genesis.address }, + }; + + accountDataUpdated(data)(dispatch); + expect(peersActionsStub).to.have.been.calledWith({ online: false, code: 'EUNAVAILABLE' }); + }); + }); + + describe('updateTransactionsIfNeeded', () => { + let transactionsActionsStub; + + const dispatch = spy(); + + beforeEach(() => { + transactionsActionsStub = spy(transactionsActions, 'transactionsUpdated'); + }); + + afterEach(() => { + transactionsActionsStub.restore(); + }); + + it('should update transactions when window is in focus', () => { + const data = { + activePeer: {}, + transactions: { confirmed: [{ confirmations: 10 }], pending: [] }, + account: { address: accounts.genesis.address }, + }; + + updateTransactionsIfNeeded(data, true)(dispatch); + expect(transactionsActionsStub).to.have.been.calledWith(); + }); + + it('should update transactions when there are no recent transactions', () => { + const data = { + activePeer: {}, + transactions: { confirmed: [{ confirmations: 10000 }], pending: [] }, + account: { address: accounts.genesis.address }, + }; + + updateTransactionsIfNeeded(data, false)(dispatch); + expect(transactionsActionsStub).to.have.been.calledWith(); + }); + }); }); diff --git a/src/actions/search.js b/src/actions/search.js index 1eb3f6c705..847ecb853f 100644 --- a/src/actions/search.js +++ b/src/actions/search.js @@ -63,7 +63,9 @@ export const searchTransactions = ({ }) => (dispatch) => { if (showLoading) loadingStarted(actionTypes.searchTransactions); - getTransactions({ activePeer, address, limit, filter }) + getTransactions({ + activePeer, address, limit, filter, + }) .then((transactionsResponse) => { dispatch({ data: { @@ -82,7 +84,9 @@ export const searchMoreTransactions = ({ activePeer, address, limit, offset, filter, }) => (dispatch) => { - getTransactions({ activePeer, address, limit, offset, filter }) + getTransactions({ + activePeer, address, limit, offset, filter, + }) .then((transactionsResponse) => { dispatch({ data: { diff --git a/src/actions/transactions.js b/src/actions/transactions.js index 41df9404b5..e8d668da62 100644 --- a/src/actions/transactions.js +++ b/src/actions/transactions.js @@ -72,9 +72,13 @@ export const loadTransactions = ({ activePeer, publicKey, address }) => }); }; -export const transactionsRequested = ({ activePeer, address, limit, offset, filter }) => +export const transactionsRequested = ({ + activePeer, address, limit, offset, filter, +}) => (dispatch) => { - getTransactions({ activePeer, address, limit, offset, filter }) + getTransactions({ + activePeer, address, limit, offset, filter, + }) .then((response) => { dispatch({ data: { @@ -144,9 +148,13 @@ export const loadTransaction = ({ activePeer, id }) => }); }; -export const transactionsUpdated = ({ activePeer, address, limit, filter, pendingTransactions }) => +export const transactionsUpdated = ({ + activePeer, address, limit, filter, pendingTransactions, +}) => (dispatch) => { - getTransactions({ activePeer, address, limit, filter }) + getTransactions({ + activePeer, address, limit, filter, + }) .then((response) => { dispatch({ data: { @@ -165,7 +173,9 @@ export const transactionsUpdated = ({ activePeer, address, limit, filter, pendin }); }; -export const sent = ({ activePeer, account, recipientId, amount, passphrase, secondPassphrase }) => +export const sent = ({ + activePeer, account, recipientId, amount, passphrase, secondPassphrase, +}) => (dispatch) => { send(activePeer, recipientId, toRawLsk(amount), passphrase, secondPassphrase) .then((data) => { diff --git a/src/actions/transactions.test.js b/src/actions/transactions.test.js index 558b1eb36d..458bcf4ea7 100644 --- a/src/actions/transactions.test.js +++ b/src/actions/transactions.test.js @@ -32,7 +32,7 @@ describe('actions: transactions', () => { transactionsApiMock.restore(); }); - it('should dispatch transactionsLoaded action if resolved', () => { + it('should dispatch transactionsUpdated action if resolved', () => { transactionsApiMock.returnsPromise().resolves({ transactions: [], count: '0' }); const expectedAction = { count: 0, diff --git a/src/store/middlewares/account.js b/src/store/middlewares/account.js index 6719b4634e..68d89c01a3 100644 --- a/src/store/middlewares/account.js +++ b/src/store/middlewares/account.js @@ -1,7 +1,4 @@ -import { getAccount } from '../../utils/api/account'; -import { accountUpdated } from '../../actions/account'; -import { transactionsUpdated } from '../../actions/transactions'; -import { activePeerUpdate } from '../../actions/peers'; +import { accountUpdated, accountDataUpdated, updateTransactionsIfNeeded } from '../../actions/account'; import { votesFetched } from '../../actions/voting'; import actionTypes from '../../constants/actions'; import accountConfig from '../../constants/account'; @@ -10,41 +7,15 @@ import transactionTypes from '../../constants/transactionTypes'; const { lockDuration } = accountConfig; -const updateTransactions = (store, peers) => { - const state = store.getState(); - const { filter } = state.transactions; - const address = state.transactions.account - ? state.transactions.account.address - : state.account.address; - - store.dispatch(transactionsUpdated({ - pendingTransactions: state.transactions.pending, - activePeer: peers.data, - address, - limit: 25, - filter, - })); -}; - -const hasRecentTransactions = txs => ( - txs.confirmed.filter(tx => tx.confirmations < 1000).length !== 0 || - txs.pending.length !== 0 -); - const updateAccountData = (store, action) => { const { peers, account, transactions } = store.getState(); - getAccount(peers.data, account.address).then((result) => { - if (result.balance !== account.balance) { - if (!action.data.windowIsFocused || !hasRecentTransactions(transactions)) { - updateTransactions(store, peers, account); - } - } - store.dispatch(accountUpdated(result)); - store.dispatch(activePeerUpdate({ online: true })); - }).catch((res) => { - store.dispatch(activePeerUpdate({ online: false, code: res.error.code })); - }); + store.dispatch(accountDataUpdated({ + windowIsFocused: action.data.windowIsFocused, + transactions, + account, + peers, + })); }; const getRecentTransactionOfType = (transactionsList, type) => ( @@ -89,23 +60,27 @@ const votePlaced = (store, action) => { }; const passphraseUsed = (store, action) => { + const data = { expireTime: Date.now() + lockDuration }; + if (!store.getState().account.passphrase) { - store.dispatch(accountUpdated({ - passphrase: action.data, - expireTime: Date.now() + lockDuration, - })); - } else { - store.dispatch(accountUpdated({ expireTime: Date.now() + lockDuration })); + data.passphrase = action.data; } + + store.dispatch(accountUpdated(data)); }; const checkTransactionsAndUpdateAccount = (store, action) => { const state = store.getState(); const { peers, account, transactions } = state; - if (action.data.windowIsFocused && hasRecentTransactions(transactions)) { - updateTransactions(store, peers, account); - } + store.dispatch(updateTransactionsIfNeeded( + { + transactions, + activePeer: peers.data, + account, + }, + action.data.windowIsFocused, + )); const tx = action.data.block.transactions; const accountAddress = state.account.address; diff --git a/src/store/middlewares/account.test.js b/src/store/middlewares/account.test.js index aee85a5428..78ec6734fe 100644 --- a/src/store/middlewares/account.test.js +++ b/src/store/middlewares/account.test.js @@ -1,9 +1,8 @@ import { expect } from 'chai'; -import { spy, stub, useFakeTimers, match } from 'sinon'; -import { accountUpdated } from '../../actions/account'; +import { spy, stub, useFakeTimers } from 'sinon'; +import * as accountActions from '../../actions/account'; import * as transactionsActions from '../../actions/transactions'; import accountConfig from '../../constants/account'; -import { activePeerUpdate } from '../../actions/peers'; import * as votingActions from '../../actions/voting'; import * as accountApi from '../../utils/api/account'; import * as transactionsApi from '../../utils/api/transactions'; @@ -43,22 +42,6 @@ describe('Account middleware', () => { }, }; - const inactiveNewBlockCreated = { - type: actionTypes.newBlockCreated, - data: { - windowIsFocused: false, - block: transactions, - }, - }; - - const blockWithNullTransaction = { - type: actionTypes.newBlockCreated, - data: { - windowIsFocused: true, - block: { transactions: [null] }, - }, - }; - let clock; beforeEach(() => { @@ -84,12 +67,14 @@ describe('Account middleware', () => { store.getState = () => (state); next = spy(); + spy(accountActions, 'updateTransactionsIfNeeded'); stubGetAccount = stub(accountApi, 'getAccount').returnsPromise(); transactionsActionsStub = spy(transactionsActions, 'transactionsUpdated'); stubTransactions = stub(transactionsApi, 'getTransactions').returnsPromise().resolves(true); }); afterEach(() => { + accountActions.updateTransactionsIfNeeded.restore(); transactionsActionsStub.restore(); stubGetAccount.restore(); stubTransactions.restore(); @@ -102,67 +87,23 @@ describe('Account middleware', () => { }); it(`should call account API methods on ${actionTypes.newBlockCreated} action when online`, () => { - // does this matter? - stubGetAccount.resolves({ balance: 0 }); - - middleware(store)(next)(newBlockCreated); - - expect(stubGetAccount).to.have.been.calledWith(); - expect(store.dispatch).to.have.been.calledWith(activePeerUpdate({ online: true })); - }); - - it(`should call account API methods on ${actionTypes.newBlockCreated} action when offline`, () => { - const errorCode = 'EUNAVAILABLE'; - stubGetAccount.rejects({ error: { code: errorCode } }); - - middleware(store)(next)(newBlockCreated); - - expect(store.dispatch).to.have.been - .calledWith(activePeerUpdate({ online: false, code: errorCode })); - }); - - it(`should call transactions API methods on ${actionTypes.newBlockCreated} action if account.balance changes`, () => { - stubGetAccount.resolves({ balance: 10e8 }); - middleware(store)(next)(newBlockCreated); - expect(stubGetAccount).to.have.been.calledWith(); - expect(transactionsActionsStub).to.have.been.calledWith(); - }); - - it(`should call transactions API methods on ${actionTypes.newBlockCreated} action if account.balance changes and the window is in blur`, () => { - stubGetAccount.resolves({ balance: 10e8 }); - - middleware(store)(next)(inactiveNewBlockCreated); - expect(stubGetAccount).to.have.been.calledWith(); - expect(transactionsActionsStub).to.have.been.calledWith(); - }); - - it(`should call transactions API methods on ${actionTypes.newBlockCreated} action if account.balance changes the user has no transactions yet`, () => { - stubGetAccount.resolves({ balance: 10e8 }); - - state.transactions.count = 0; - middleware(store)(next)(newBlockCreated); - - expect(stubGetAccount).to.have.been.calledWith(); - // eslint-disable-next-line no-unused-expressions - expect(transactionsActionsStub).to.have.been.calledOnce; - }); - - it(`should call transactions API methods on ${actionTypes.newBlockCreated} action if the window is in focus and there are recent transactions`, () => { - stubGetAccount.resolves({ balance: 0 }); - + const accountDataUpdatedSpy = spy(accountActions, 'accountDataUpdated'); middleware(store)(next)(newBlockCreated); - expect(stubGetAccount).to.have.been.calledWith(); - expect(transactionsActionsStub).to.have.been.calledWith(match({ address: 'test_address' })); - }); - it(`should call transactions API methods on ${actionTypes.newBlockCreated} action if block.transactions contains null element`, () => { - middleware(store)(next)(blockWithNullTransaction); + const data = { + windowIsFocused: true, + peers: state.peers, + account: state.account, + transactions: state.transactions, + }; - expect(transactionsActionsStub).to.have.been.calledWith(); + expect(accountDataUpdatedSpy).to.have.been.calledWith(data); + expect(accountActions.updateTransactionsIfNeeded).to.have.been.calledWith(); + accountDataUpdatedSpy.restore(); }); - it(`should call API methods on ${actionTypes.newBlockCreated} action if state.transaction.transactions.confired does not contain recent transaction. Case with transactions address`, () => { - stubGetAccount.resolves({ balance: 0 }); + it(`should call API methods on ${actionTypes.newBlockCreated} action if state.transaction.transactions.confirmed does not contain recent transaction. Case with transactions address`, () => { + const accountDataUpdatedSpy = spy(accountActions, 'accountDataUpdated'); store.getState = () => ({ ...state, @@ -174,12 +115,12 @@ describe('Account middleware', () => { }); middleware(store)(next)(newBlockCreated); - expect(stubGetAccount).to.have.been.calledWith({}); - expect(transactionsActionsStub).to.have.been.calledWith(); + expect(accountDataUpdatedSpy).to.have.been.calledWith(); + accountDataUpdatedSpy.restore(); }); - it(`should call API methods on ${actionTypes.newBlockCreated} action if state.transaction.transactions.confired does not contain recent transaction. Case with confirmed address`, () => { - stubGetAccount.resolves({ balance: 0 }); + it(`should call API methods on ${actionTypes.newBlockCreated} action if state.transaction.transactions.confirmed does not contain recent transaction. Case with confirmed address`, () => { + const accountDataUpdatedSpy = spy(accountActions, 'accountDataUpdated'); store.getState = () => ({ ...state, @@ -192,8 +133,8 @@ describe('Account middleware', () => { }); middleware(store)(next)(newBlockCreated); - expect(stubGetAccount).to.have.been.calledWith({}); - expect(transactionsActionsStub).to.have.been.calledWith(); + expect(accountDataUpdatedSpy).to.have.been.calledWith(); + accountDataUpdatedSpy.restore(); }); it(`should fetch delegate info on ${actionTypes.transactionsUpdated} action if action.data.confirmed contains delegateRegistration transactions`, () => { @@ -227,16 +168,22 @@ describe('Account middleware', () => { }); it(`should dispatch accountUpdated({passphrase}) action on ${actionTypes.passphraseUsed} action if store.account.passphrase is not set`, () => { + const accountUpdatedSpy = spy(accountActions, 'accountUpdated'); + const action = { type: actionTypes.passphraseUsed, data: passphrase, }; middleware(store)(next)(action); - expect(store.dispatch).to.have.been - .calledWith(accountUpdated({ passphrase, expireTime: clock.now + lockDuration })); + expect(accountUpdatedSpy).to.have.been.calledWith({ + passphrase, + expireTime: clock.now + lockDuration, + }); + accountUpdatedSpy.restore(); }); it(`should not dispatch accountUpdated action on ${actionTypes.passphraseUsed} action if store.account.passphrase is already set`, () => { + const accountUpdatedSpy = spy(accountActions, 'accountUpdated'); const action = { type: actionTypes.passphraseUsed, data: passphrase, @@ -245,8 +192,9 @@ describe('Account middleware', () => { ...state, account: { ...state.account, passphrase, expireTime: clock.now + lockDuration }, }); + middleware(store)(next)(action); - expect(store.dispatch).to.have.been - .calledWith(accountUpdated({ expireTime: clock.now + lockDuration })); + expect(accountUpdatedSpy).to.have.been.calledWith({ expireTime: clock.now + lockDuration }); + accountUpdatedSpy.restore(); }); }); diff --git a/src/utils/api/transactions.js b/src/utils/api/transactions.js index 90494b00b0..a744c54111 100644 --- a/src/utils/api/transactions.js +++ b/src/utils/api/transactions.js @@ -2,10 +2,16 @@ import { requestToActivePeer } from './peers'; import txFilters from './../../constants/transactionFilters'; export const send = (activePeer, recipientId, amount, secret, secondSecret = null) => - requestToActivePeer(activePeer, 'transactions', - { recipientId, amount, secret, secondSecret }); + requestToActivePeer( + activePeer, 'transactions', + { + recipientId, amount, secret, secondSecret, + }, + ); -export const getTransactions = ({ activePeer, address, limit = 20, offset = 0, orderBy = 'timestamp:desc', filter = txFilters.all }) => { +export const getTransactions = ({ + activePeer, address, limit = 20, offset = 0, orderBy = 'timestamp:desc', filter = txFilters.all, +}) => { let params = { recipientId: (filter === txFilters.incoming || filter === txFilters.all) ? address : undefined, senderId: (filter === txFilters.outgoing || filter === txFilters.all) ? address : undefined,