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

Handle failed pending transactions - Closes #738 #846

Merged
merged 7 commits into from
Oct 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/actions/transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ export const transactionAdded = data => ({
type: actionTypes.transactionAdded,
});

/**
* An action to dispatch transactionsFailed
*
*/
export const transactionsFailed = data => ({
data,
type: actionTypes.transactionsFailed,
});

/**
* An action to dispatch transactionsUpdated
*
Expand Down
16 changes: 15 additions & 1 deletion src/actions/transactions.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import sinon from 'sinon';
import actionTypes from '../constants/actions';
import { transactionAdded, transactionsUpdated,
import { transactionAdded, transactionsUpdated, transactionsFailed,
transactionsLoaded, transactionsRequested } from './transactions';
import * as accountApi from '../utils/api/account';

Expand All @@ -20,6 +20,20 @@ describe('actions: transactions', () => {
});
});

describe('transactionsFailed', () => {
it('should create an action to transactionsFailed', () => {
const data = {
id: 'dummy',
};
const expectedAction = {
data,
type: actionTypes.transactionsFailed,
};

expect(transactionsFailed(data)).to.be.deep.equal(expectedAction);
});
});

describe('transactionsUpdated', () => {
it('should create an action to transactionsUpdated', () => {
const data = {
Expand Down
1 change: 1 addition & 0 deletions src/constants/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const actionTypes = {
loadingStarted: 'LOADING_STARTED',
loadingFinished: 'LOADING_FINISHED',
transactionAdded: 'TRANSACTION_ADDED',
transactionsFailed: 'TRANSACTIONS_FAILED',
transactionsUpdated: 'TRANSACTIONS_UPDATED',
transactionsLoaded: 'TRANSACTIONS_LOADED',
transactionsReset: 'TRANSACTIONS_RESET',
Expand Down
24 changes: 0 additions & 24 deletions src/store/middlewares/addedTransaction.js

This file was deleted.

58 changes: 0 additions & 58 deletions src/store/middlewares/addedTransaction.test.js

This file was deleted.

4 changes: 2 additions & 2 deletions src/store/middlewares/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import thunk from 'redux-thunk';
import metronomeMiddleware from './metronome';
import accountMiddleware from './account';
import loginMiddleware from './login';
import addedTransactionMiddleware from './addedTransaction';
import transactionsMiddleware from './transactions';
import loadingBarMiddleware from './loadingBar';
import offlineMiddleware from './offline';
import notificationMiddleware from './notification';
Expand All @@ -11,7 +11,7 @@ import savedAccountsMiddleware from './savedAccounts';

export default [
thunk,
addedTransactionMiddleware,
transactionsMiddleware,
loginMiddleware,
metronomeMiddleware,
accountMiddleware,
Expand Down
48 changes: 48 additions & 0 deletions src/store/middlewares/transactions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import i18next from 'i18next';

import { fromRawLsk } from '../../utils/lsk';
import { unconfirmedTransactions } from '../../utils/api/account';
import { successAlertDialogDisplayed } from '../../actions/dialog';
import { transactionsFailed } from '../../actions/transactions';
import actionTypes from '../../constants/actions';
import transactionTypes from '../../constants/transactionTypes';

const transactionAdded = (store, action) => {
const texts = {
[transactionTypes.setSecondPassphrase]: i18next.t('Second passphrase registration was successfully submitted. It can take several seconds before it is processed.'),
[transactionTypes.registerDelegate]: i18next.t('Delegate registration was successfully submitted with username: "{{username}}". It can take several seconds before it is processed.',
{ username: action.data.username }),
[transactionTypes.vote]: i18next.t('Your votes were successfully submitted. It can take several seconds before they are processed.'),
[transactionTypes.send]: i18next.t('Your transaction of {{amount}} LSK to {{recipientAddress}} was accepted and will be processed in a few seconds.',
{ amount: fromRawLsk(action.data.amount), recipientAddress: action.data.recipientId }),
};
const text = texts[action.data.type];
const newAction = successAlertDialogDisplayed({ text });
store.dispatch(newAction);
};

const transactionsUpdated = (store) => {
const { transactions, account, peers } = store.getState();
if (transactions.pending.length) {
unconfirmedTransactions(peers.data, account.address)
.then(response => store.dispatch(transactionsFailed({
failed: transactions.pending.filter(tx =>
response.transactions.filter(unconfirmedTx => tx.id === unconfirmedTx.id).length === 0),
})));
}
};

const transactionsMiddleware = store => next => (action) => {
next(action);
switch (action.type) {
case actionTypes.transactionAdded:
transactionAdded(store, action);
break;
case actionTypes.transactionsUpdated:
transactionsUpdated(store, action);
break;
default: break;
}
};

export default transactionsMiddleware;
107 changes: 107 additions & 0 deletions src/store/middlewares/transactions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { expect } from 'chai';
import { spy, stub, mock } from 'sinon';
import i18next from 'i18next';
import * as accountApi from '../../utils/api/account';
import { successAlertDialogDisplayed } from '../../actions/dialog';
import { transactionsFailed } from '../../actions/transactions';
import middleware from './transactions';
import actionTypes from '../../constants/actions';

describe('transaction middleware', () => {
let store;
let next;
let state;
let accountApiMock;
const mockTransaction = {
username: 'test',
amount: 1e8,
recipientId: '16313739661670634666L',
};

beforeEach(() => {
store = stub();
state = {
peers: {
data: {},
},
account: {
address: '8096217735672704724L',
},
transactions: {
pending: [],
},
};
store.getState = () => (state);
store.dispatch = spy();
next = spy();
accountApiMock = mock(accountApi);
});

afterEach(() => {
accountApiMock.restore();
});

it('should passes the action to next middleware', () => {
const givenAction = {
type: 'TEST_ACTION',
};

middleware(store)(next)(givenAction);
expect(next).to.have.been.calledWith(givenAction);
});

it('should fire success dialog action with appropriate text if action.type is transactionAdded', () => {
const givenAction = {
type: actionTypes.transactionAdded,
data: mockTransaction,
};

const expectedMessages = [
'Your transaction of 1 LSK to 16313739661670634666L was accepted and will be processed in a few seconds.',
'Second passphrase registration was successfully submitted. It can take several seconds before it is processed.',
'Delegate registration was successfully submitted with username: "test". It can take several seconds before it is processed.',
'Your votes were successfully submitted. It can take several seconds before they are processed.',
];

for (let i = 0; i < 4; i++) {
givenAction.data.type = i;
middleware(store)(next)(givenAction);
const expectedAction = successAlertDialogDisplayed({ text: i18next.t(expectedMessages[i]) });
expect(store.dispatch).to.have.been.calledWith(expectedAction);
}
});

it('should do nothing if state.transactions.pending.length === 0 and action.type is transactionsUpdated', () => {
const givenAction = {
type: actionTypes.transactionsUpdated,
data: [mockTransaction],
};

middleware(store)(next)(givenAction);
expect(store.dispatch).to.not.have.been.calledWith();
});

it('should call unconfirmedTransactions and then dispatch transactionsFailed if state.transactions.pending.length > 0 and action.type is transactionsUpdated', () => {
const transactions = [
mockTransaction,
];
accountApiMock.expects('unconfirmedTransactions')
.withExactArgs(state.peers.data, state.account.address)
.returnsPromise().resolves({ transactions });
store.getState = () => ({
...state,
transactions: {
pending: transactions,
},
});
const givenAction = {
type: actionTypes.transactionsUpdated,
data: [],
};

middleware(store)(next)(givenAction);
const expectedAction = transactionsFailed({ failed: [] });
expect(store.dispatch).to.have.been.calledWith(expectedAction);
});
});

7 changes: 7 additions & 0 deletions src/store/reducers/transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const transactions = (state = { pending: [], confirmed: [], count: null }, actio
return Object.assign({}, state, {
pending: [action.data, ...state.pending],
});
case actionTypes.transactionsFailed:
return Object.assign({}, state, {
// Filter any failed transaction from pending
pending: state.pending.filter(
pendingTransaction => action.data.failed.filter(
transaction => transaction.id === pendingTransaction.id).length === 0),
});
case actionTypes.transactionsLoaded:
return Object.assign({}, state, {
confirmed: [
Expand Down
Loading