Skip to content
This repository has been archived by the owner on Apr 15, 2019. It is now read-only.

Make sure confirmations are updated - Closes #716 #735

Merged
merged 11 commits into from
Sep 14, 2017
2 changes: 1 addition & 1 deletion features/transactions.feature
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Feature: Transactions tab
Scenario: should show transactions
Given I'm logged in as "genesis"
When I click tab number 1
Then I should see table with 40 lines
Then I should see table with 25 lines

Scenario: should allow send to address
Given I'm logged in as "genesis"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"flexboxgrid": "=6.3.1",
"lisk-js": "=0.4.5",
"moment": "=2.15.1",
"numeral": "=2.0.6",
"postcss": "=6.0.2",
"postcss-cssnext": "=2.11.0",
"prop-types": "=15.5.10",
Expand Down
55 changes: 32 additions & 23 deletions src/components/transactions/transactionRow.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import numeral from 'numeral';
import LiskAmount from '../liskAmount';
import { TooltipTime, TooltipWrapper } from '../timestamp';
import TransactionType from './transactionType';
Expand All @@ -7,31 +8,39 @@ import Status from './status';
import Amount from './amount';
import Spinner from '../spinner';

const TransactionRow = props => (
<tr>
<td className={`${props.tableStyle.rowCell} ${styles.centerText}`}>
class TransactionRow extends React.Component {
// eslint-disable-next-line class-methods-use-this
shouldComponentUpdate(nextProps) {
return nextProps.value.confirmations <= 1000;
}

render() {
const props = this.props;
return (<tr>
<td className={`${props.tableStyle.rowCell} ${styles.centerText}`}>
{props.value.confirmations ?
<TooltipTime label={props.value.timestamp}></TooltipTime> :
<Spinner />}
</td>
<td className={`${props.tableStyle.rowCell} ${styles.centerText} ${styles.hiddenXs}`}>
<TooltipWrapper tooltip={`${props.value.confirmations || 0} confirmation${props.value.confirmations !== 1 ? 's' : ''}`}>{props.value.id}</TooltipWrapper>
</td>
<td className={`${props.tableStyle.rowCell} ${styles.centerText}`}>
<TransactionType {...props.value} address={props.address}></TransactionType>
</td>
<td className={`${props.tableStyle.rowCell} ${styles.centerText}`}>
<Status {...props}></Status>
</td>
<td className={`${props.tableStyle.rowCell} ${styles.centerText}`}>
<Amount {...props}></Amount>
</td>
<td className={`${props.tableStyle.rowCell} ${styles.centerText}`}>
<span className={styles.grayButton}>
<LiskAmount val={props.value.fee} />
</span>
</td>
</tr>
);
</td>
<td className={`${props.tableStyle.rowCell} ${styles.centerText} ${styles.hiddenXs}`}>
<TooltipWrapper tooltip={`${numeral(props.value.confirmations || 0).format('0a')} confirmation${props.value.confirmations !== 1 ? 's' : ''}`}>{props.value.id}</TooltipWrapper>
</td>
<td className={`${props.tableStyle.rowCell} ${styles.centerText}`}>
<TransactionType {...props.value} address={props.address}></TransactionType>
</td>
<td className={`${props.tableStyle.rowCell} ${styles.centerText}`}>
<Status {...props}></Status>
</td>
<td className={`${props.tableStyle.rowCell} ${styles.centerText}`}>
<Amount {...props}></Amount>
</td>
<td className={`${props.tableStyle.rowCell} ${styles.centerText}`}>
<span className={styles.grayButton}>
<LiskAmount val={props.value.fee} />
</span>
</td>
</tr>);
}
}

export default TransactionRow;
6 changes: 0 additions & 6 deletions src/components/transactions/transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ class Transactions extends React.Component {
}
}

shouldComponentUpdate(nextProps) {
const shouldUpdate = ((nextProps.confirmedCount !== this.props.confirmedCount) ||
(nextProps.pendingCount !== this.props.pendingCount));
return shouldUpdate;
}

componentDidUpdate() {
this.canLoadMore = this.props.count > this.props.transactions.length;
}
Expand Down
34 changes: 25 additions & 9 deletions src/store/middlewares/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,34 @@ import actionTypes from '../../constants/actions';
import { fetchAndUpdateForgedBlocks } from '../../actions/forging';
import { getDelegate } from '../../utils/api/delegate';
import transactionTypes from '../../constants/transactionTypes';
import { SYNC_ACTIVE_INTERVAL, SYNC_INACTIVE_INTERVAL } from '../../constants/api';

const updateAccountData = (store) => { // eslint-disable-line
const { peers, account } = store.getState();
const updateTransactions = (store, peers, account) => {
const maxBlockSize = 25;
transactions(peers.data, account.address, maxBlockSize)
.then(response => store.dispatch(transactionsUpdated({
confirmed: response.transactions,
count: parseInt(response.count, 10),
})));
};

const hasRecentTransactions = state => (
state.transactions.confirmed.filter(tx => tx.confirmations < 1000).length !== 0 ||
state.transactions.pending.length !== 0
);

const updateAccountData = (store, action) => { // eslint-disable-line
const state = store.getState();
const { peers, account } = state;

getAccount(peers.data, account.address).then((result) => {
if (action.data.interval === SYNC_ACTIVE_INTERVAL && hasRecentTransactions(state)) {
updateTransactions(store, peers, account);
}
if (result.balance !== account.balance) {
const maxBlockSize = 25;
transactions(peers.data, account.address, maxBlockSize)
.then(response => store.dispatch(transactionsUpdated({
confirmed: response.transactions,
count: parseInt(response.count, 10),
})));
if (action.data.interval === SYNC_INACTIVE_INTERVAL) {
updateTransactions(store, peers, account);
}
if (account.isDelegate) {
store.dispatch(fetchAndUpdateForgedBlocks({
activePeer: peers.data,
Expand Down Expand Up @@ -55,7 +71,7 @@ const accountMiddleware = store => next => (action) => {
next(action);
switch (action.type) {
case actionTypes.metronomeBeat:
updateAccountData(store);
updateAccountData(store, action);
break;
case actionTypes.transactionsUpdated:
delegateRegistration(store, action);
Expand Down
91 changes: 74 additions & 17 deletions src/store/middlewares/account.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import * as accountApi from '../../utils/api/account';
import * as delegateApi from '../../utils/api/delegate';
import actionTypes from '../../constants/actions';
import transactionTypes from '../../constants/transactionTypes';
import { SYNC_ACTIVE_INTERVAL, SYNC_INACTIVE_INTERVAL } from '../../constants/api';

describe('Account middleware', () => {
let store;
let next;
let state;
let stubGetAccount;
let stubGetAccountStatus;
let stubTransactions;

const transactionsUpdatedAction = {
type: actionTypes.transactionsUpdated,
data: {
Expand All @@ -19,6 +24,20 @@ describe('Account middleware', () => {
},
};

const activeBeatAction = {
type: actionTypes.metronomeBeat,
data: {
interval: SYNC_ACTIVE_INTERVAL,
},
};

const inactiveBeatAction = {
type: actionTypes.metronomeBeat,
data: {
interval: SYNC_INACTIVE_INTERVAL,
},
};

beforeEach(() => {
store = stub();
store.dispatch = spy();
Expand All @@ -29,12 +48,28 @@ describe('Account middleware', () => {
account: {
balance: 0,
},
transactions: {
pending: [{
id: 12498250891724098,
}],
confirmed: [],
},
};
store.getState = () => (state);

next = spy();
stubGetAccount = stub(accountApi, 'getAccount').returnsPromise();
stubGetAccountStatus = stub(accountApi, 'getAccountStatus').returnsPromise();
stubTransactions = stub(accountApi, 'transactions').returnsPromise().resolves(true);
});

afterEach(() => {
stubGetAccount.restore();
stubGetAccountStatus.restore();
stubTransactions.restore();
});

it('should passes the action to next middleware', () => {
it('should pass the action to next middleware', () => {
const expectedAction = {
type: 'TEST_ACTION',
};
Expand All @@ -44,46 +79,68 @@ describe('Account middleware', () => {
});

it(`should call account API methods on ${actionTypes.metronomeBeat} action`, () => {
const stubGetAccount = stub(accountApi, 'getAccount').resolves({ balance: 0 });
const stubGetAccountStatus = stub(accountApi, 'getAccountStatus').resolves(true);
stubGetAccount.resolves({ balance: 0 });

middleware(store)(next)({ type: actionTypes.metronomeBeat });
middleware(store)(next)(activeBeatAction);

expect(stubGetAccount).to.have.been.calledWith();
expect(stubGetAccountStatus).to.have.been.calledWith();

stubGetAccount.restore();
stubGetAccountStatus.restore();
});

it(`should call transactions API methods on ${actionTypes.metronomeBeat} action if account.balance changes`, () => {
const stubGetAccount = stub(accountApi, 'getAccount').resolves({ balance: 10e8 });
const stubTransactions = stub(accountApi, 'transactions').resolves(true);
stubGetAccount.resolves({ balance: 10e8 });
stubGetAccountStatus.resolves(true);

middleware(store)(next)({ type: actionTypes.metronomeBeat });
middleware(store)(next)(activeBeatAction);

expect(stubGetAccount).to.have.been.calledWith();
// TODO why next expect doesn't work despite it being called according to test coverage?
// expect(stubTransactions).to.have.been.calledWith();
});

stubGetAccount.restore();
stubTransactions.restore();
it(`should call transactions API methods on ${actionTypes.metronomeBeat} action if account.balance changes and action.data.interval is SYNC_INACTIVE_INTERVAL`, () => {
stubGetAccount.resolves({ balance: 10e8 });
stubGetAccountStatus.rejects(false);

middleware(store)(next)(inactiveBeatAction);

expect(stubGetAccount).to.have.been.calledWith();
// TODO why next expect doesn't work despite it being called according to test coverage?
// expect(stubTransactions).to.have.been.calledWith();
});

it(`should call transactions API methods on ${actionTypes.metronomeBeat} action if action.data.interval is SYNC_ACTIVE_INTERVAL and there are recent transactions`, () => {
stubGetAccount.resolves({ balance: 0 });

middleware(store)(next)(activeBeatAction);

expect(stubGetAccount).to.have.been.calledWith();
// TODO why next expect doesn't work despite it being called according to test coverage?
// expect(stubTransactions).to.have.been.calledWith();
});

it(`should fetch delegate info on ${actionTypes.metronomeBeat} action if account.balance changes and account.isDelegate`, () => {
const delegateApiMock = stub(delegateApi, 'getDelegate').returnsPromise().resolves({ success: true, delegate: {} });
stubGetAccount.resolves({ balance: 10e8 });
state.account.isDelegate = true;
store.getState = () => (state);

middleware(store)(next)(activeBeatAction);
expect(store.dispatch).to.have.been.calledWith();

delegateApiMock.restore();
});

it(`should call fetchAndUpdateForgedBlocks(...) on ${actionTypes.metronomeBeat} action if account.balance changes and account.isDelegate`, () => {
state.account.isDelegate = true;
store.getState = () => (state);
const stubGetAccount = stub(accountApi, 'getAccount').resolves({ balance: 10e8 });
const stubGetAccountStatus = stub(accountApi, 'getAccountStatus').resolves(true);
stubGetAccount.resolves({ balance: 10e8 });
// const fetchAndUpdateForgedBlocksSpy = spy(forgingActions, 'fetchAndUpdateForgedBlocks');

middleware(store)(next)({ type: actionTypes.metronomeBeat });

// TODO why next expect doesn't work despite it being called according to test coverage?
// expect(fetchAndUpdateForgedBlocksSpy).to.have.been.calledWith();

stubGetAccount.restore();
stubGetAccountStatus.restore();
});

it(`should fetch delegate info on ${actionTypes.transactionsUpdated} action if action.data.confirmed contains delegateRegistration transactions`, () => {
Expand Down
11 changes: 4 additions & 7 deletions src/store/reducers/transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import actionTypes from '../../constants/actions';
* @param {Object} action
*/
const transactions = (state = { pending: [], confirmed: [], count: 0 }, action) => {
let startTimestamp;

switch (action.type) {
case actionTypes.transactionAdded:
return Object.assign({}, state, {
Expand All @@ -22,18 +20,17 @@ const transactions = (state = { pending: [], confirmed: [], count: 0 }, action)
count: action.data.count,
});
case actionTypes.transactionsUpdated:
startTimestamp = state.confirmed.length ?
state.confirmed[0].timestamp :
0;
return Object.assign({}, state, {
// Filter any newly confirmed transaction from pending
pending: state.pending.filter(
pendingTransaction => action.data.confirmed.filter(
transaction => transaction.id === pendingTransaction.id).length === 0),
// Add any newly confirmed transaction to confirmed
confirmed: [
...action.data.confirmed.filter(transaction => transaction.timestamp > startTimestamp),
...state.confirmed,
...action.data.confirmed,
...state.confirmed.filter(
confirmedTransaction => action.data.confirmed.filter(
transaction => transaction.id === confirmedTransaction.id).length === 0),
],
count: action.data.count,
});
Expand Down
9 changes: 5 additions & 4 deletions src/utils/metronome.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ class Metronome {
* @param {Date} lastBeat
* @param {Date} now
* @param {Number} factor
* @param {Number} interval
* @memberOf Metronome
* @private
*/
_dispatch(lastBeat, now, factor) {
_dispatch(lastBeat, now, factor, interval) {
this.dispatchFn({
type: actionsType.metronomeBeat,
data: { lastBeat, now, factor },
data: { lastBeat, now, factor, interval },
});
}

/**
/**
* We're calling this in framerate.
* calls broadcast method every SYNC_(IN)ACTIVE_INTERVAL and
* sends a numeric factor for ease of use as multiples of updateInterval.
Expand All @@ -38,7 +39,7 @@ class Metronome {
_step() {
const now = new Date();
if (!this.lastBeat || (now - this.lastBeat >= this.interval)) {
this._dispatch(this.lastBeat, now, this.factor);
this._dispatch(this.lastBeat, now, this.factor, this.interval);
this.lastBeat = now;
this.factor += this.factor < 9 ? 1 : -9;
}
Expand Down