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

Commit

Permalink
Merge pull request #846 from LiskHQ/738-handle-failed-transactions
Browse files Browse the repository at this point in the history
Handle failed pending transactions - Closes #738
  • Loading branch information
slaweet authored Oct 12, 2017
2 parents 8bc1ac2 + cbeb12c commit c36a074
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 98 deletions.
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

0 comments on commit c36a074

Please sign in to comment.