Skip to content
This repository has been archived by the owner on Jan 14, 2022. It is now read-only.

Implement generic transaction page - Closes #813 #818

Merged
merged 14 commits into from
Nov 26, 2018
4 changes: 4 additions & 0 deletions api/transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ module.exports = [
path: 'getLastTransactions',
service: 'transactions',
params: () => undefined,
}, {
path: 'getTransactions',
service: 'transactions',
params: req => req.query,
}, {
path: 'getTransactionsByAddress',
service: 'transactions',
Expand Down
20 changes: 20 additions & 0 deletions features/transaction.feature
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,23 @@ Feature: Transaction page
And I should see "genesis_51 • genesis_2 • genesis_7 • genesis_3 • genesis_4 • genesis_5 • genesis_6 • genesis_8 • genesis_9 • genesis_10 • genesis_11" in "deleted votes" element
And I click "vote deleted link" no. 1
Then I should be on page "/address/2581762640681118072L"

Scenario: should show transactions list
Given I'm on page "/txs/"
Then I should see "Transactions" in "h1" html element
And I should see "Home Transactions" in "breadcrumb" element
And I should see table "transactions" with 50 rows starting with:
| Transaction ID | Date | Sender | Recipient | Amount | Fee | Confirm. |
|----------------|-------------------------------|--------------------------|------------------|------------------|---------|-----------|
| /\d{18,20}/ | /2017\/06\/19 \d\d:\d\d:\d\d/ | /standby_\d{3}\|\d{20}L/ | Explorer Account | 123.45 LSK | 0.1 LSK | 5 / 101 |
| /\d{18,20}/ | /2017\/06\/19 \d\d:\d\d:\d\d/ | /standby_\d{3}\|\d{20}L/ | Explorer Account | 100 LSK | 0.1 LSK | 6 / 101 |
| /\d{18,20}/ | /2017\/06\/19 \d\d:\d\d:\d\d/ | /standby_\d{3}\|\d{20}L/ | Explorer Account | 100.12345678 LSK | 0.1 LSK | 7 / 101 |
| /\d{18,20}/ | /2017\/06\/19 \d\d:\d\d:\d\d/ | /standby_\d{3}\|\d{20}L/ | Explorer Account | 0.123456 LSK | 0.1 LSK | 8 / 101 |
| /\d{18,20}/ | /2017\/06\/19 \d\d:\d\d:\d\d/ | /standby_\d{3}\|\d{20}L/ | Explorer Account | 123.4567 LSK | 0.1 LSK | 9 / 101 |

Scenario: should allow to load more transactions
Given I'm on page "/txs/"
And I should see table "transactions" with 50 rows
When I scroll to "more button"
And I click "more button"
Then I should see table "transactions" with 100 rows
60 changes: 35 additions & 25 deletions lib/api/transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,58 +264,55 @@ module.exports = function (app) {
};

const normalizeTransactionParams = (params) => {
if (!params || (!params.address && !params.senderId && !params.recipientId)) {
return 'Missing/Invalid address parameter';
}

const directionQueries = [];
const baseQuery = `sort=timestamp:desc&offset=${param(params.offset, 0)}&limit=${param(params.limit, 100)}`;

const offset = param(params.offset, 0);
const limit = param(params.limit, 100);
const sort = params.sort || 'timestamp:desc';
const address = coreUtils.parseAddress(params.address);

if (params.direction === 'sent') {
directionQueries.push(`${baseQuery}&senderId=${address}&type=0`);
} else if (params.direction === 'received') {
directionQueries.push(`${baseQuery}&recipientId=${address}&type=0`);
} else if (params.direction === 'others') {
for (let i = 1; i < 8; i++) {
directionQueries.push(`${baseQuery}&senderId=${address}&type=${i}`);
let baseQuery = `sort=${sort}&offset=${offset}&limit=${limit}`;

if (address) {
if (params.direction === 'sent') {
directionQueries.push(`${baseQuery}&senderId=${address}&type=0`);
} else if (params.direction === 'received') {
directionQueries.push(`${baseQuery}&recipientId=${address}&type=0`);
} else if (params.direction === 'others') {
for (let i = 1; i < 8; i++) {
directionQueries.push(`${baseQuery}&senderId=${address}&type=${i}`);
}
} else {
directionQueries.push(`${baseQuery}&senderIdOrRecipientId=${address}`);
}
} else if (params.address) {
directionQueries.push(`${baseQuery}&senderIdOrRecipientId=${address}`);
} else {
// advanced search
const offset = param(params.offset, 0);
const limit = param(params.limit, 100);
const sort = params.sort || 'timestamp:desc';
let filters = ['recipientId', 'recipientPublicKey', 'senderId', 'senderPublicKey', 'height', 'minAmount', 'maxAmount', 'fromTimestamp', 'toTimestamp', 'blockId'];

let advancedQuery = `sort=${sort}&offset=${offset}&limit=${limit}`;

// If recipientId is the same as senderId, use senderIdOrRecipientId instead.
if (params.senderId && params.recipientId && params.senderId === params.recipientId) {
advancedQuery += `&senderIdOrRecipientId=${params.recipientId}`;
baseQuery += `&senderIdOrRecipientId=${params.recipientId}`;
filters = filters.filter(item => item !== 'senderId' && item !== 'recipientId');
}

Object.keys(params).forEach((key) => {
if ((filters.indexOf(key) >= 0)) {
advancedQuery += `&${key}=${params[key]}`;
baseQuery += `&${key}=${params[key]}`;
}
});

// type might be comma separate or undefined
if (params.type) {
params.type.split(',').forEach(type => directionQueries.push(`${advancedQuery}&type=${type}`));
params.type.split(',').forEach(type => directionQueries.push(`${baseQuery}&type=${type}`));
} else {
directionQueries.push(`${advancedQuery}`);
directionQueries.push(`${baseQuery}`);
}
}

return directionQueries;
};

this.getTransactionsByAddress = function (query, error, success) {
const queryList = normalizeTransactionParams(query, error);
this.getTransactionsCall = function (queryList, error, success) {
if (typeof queryList === 'string') {
return error({ success: false, error: queryList });
}
Expand Down Expand Up @@ -355,6 +352,19 @@ module.exports = function (app) {
});
};

this.getTransactions = function (query, error, success) {
const queryList = normalizeTransactionParams(query);
this.getTransactionsCall(queryList, error, success);
};

this.getTransactionsByAddress = function (query, error, success) {
if (!query || (!query.address && !query.senderId && !query.recipientId)) {
return error({ success: false, error: 'Missing/Invalid address parameter' });
}
const queryList = normalizeTransactionParams(query);
return this.getTransactionsCall(queryList, error, success);
};

this.getTransactionsByBlock = function (query, error, success) {
if (!query.blockId) {
return error({ success: false, error: 'Missing/Invalid blockId parameter' });
Expand Down
7 changes: 6 additions & 1 deletion src/app/states.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ App.config(($stateProvider, $urlRouterProvider, $locationProvider) => {
parentDir: 'home',
component: 'block',
})
.state('transactions', {
url: '/txs/:page',
parentDir: 'home',
component: 'transactions',
})
.state('transaction', {
url: '/tx/:txId',
parentDir: 'home',
component: 'transactions',
component: 'transaction',
})
.state('address', {
url: '/address/:address',
Expand Down
3 changes: 2 additions & 1 deletion src/assets/styles/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -1378,7 +1378,8 @@ table tbody tr:nth-of-type(odd) {
margin-right: 2.4rem;
}

.paginator .btn-group .see-all-blocks {
.paginator .btn-group .see-all-blocks,
.paginator .btn-group .see-all-transactions {
width: 50%;
}

Expand Down
110 changes: 0 additions & 110 deletions src/components/address/address.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,12 @@ import AppAddress from './address.module';
import template from './address.html';

const AddressConstructor = function (
$rootScope,
$stateParams,
$location,
$http,
$timeout,
addressTxs,
) {
const vm = this;
vm.searchModel = [];

vm.getAddress = () => {
$http.get('/api/getAccount', {
Expand All @@ -36,7 +33,6 @@ const AddressConstructor = function (
}).then((resp) => {
if (resp.data.success) {
vm.address = resp.data;
vm.disableAutocomplete();
vm.getVotes(vm.address.publicKey);
} else {
throw new Error('Account was not found!');
Expand All @@ -58,44 +54,12 @@ const AddressConstructor = function (
address: $stateParams.address,
};

// Sets autocomplete attr off
vm.disableAutocomplete = () => {
$timeout(() => {
document.getElementsByClassName('search-parameter-input')[0].setAttribute('autocomplete', 'off');
}, 0);
};

// Sets the filter for which transactions to display
vm.filterTxs = (direction) => {
vm.direction = direction;
vm.txs = addressTxs({ address: $stateParams.address, direction });
};

vm.searchParams = [];
vm.availableSearchParams = [
{ key: 'senderId', name: 'Sender', placeholder: 'Sender...' },
{ key: 'recipientId', name: 'Recipient', placeholder: 'Recipient...' },
{ key: 'minAmount', name: 'Min', placeholder: 'Min Amount...' },
{ key: 'maxAmount', name: 'Max', placeholder: 'Max Amount...' },
{ key: 'type', name: 'Type', placeholder: 'Comma separated...' },
{ key: 'senderPublicKey', name: 'SenderPk', placeholder: 'Sender Public Key...' },
{ key: 'recipientPublicKey', name: 'RecipientPk', placeholder: 'Recipient Public Key...' },
{ key: 'height', name: 'Block Height', placeholder: 'Block Id...' },
{ key: 'blockId', name: 'Block Id', placeholder: 'Block Id...' },
{ key: 'fromTimestamp', name: 'fromTimestamp', placeholder: 'From Timestamp...' },
{ key: 'toTimestamp', name: 'toTimestamp', placeholder: 'To Timestamp...' },
{ key: 'limit', name: 'limit', placeholder: 'Limit...' },
{ key: 'offset', name: 'offset', placeholder: 'Offset...' },
{
key: 'sort',
name: 'orderBy',
placeholder: 'Order By...',
restrictToSuggestedValues: true,
suggestedValues: ['amount:asc', 'amount:desc', 'fee:asc', 'fee:desc', 'type:asc', 'type:desc', 'timestamp:asc', 'timestamp:desc'],
},
];
vm.parametersDisplayLimit = vm.availableSearchParams.length;

vm.onFiltersUsed = () => {
vm.cleanByFilters = true;
const { removeAll } = angular.element(document.getElementsByClassName('search-parameter-input')[0]).scope();
Expand All @@ -104,80 +68,6 @@ const AddressConstructor = function (
}
};

const onSearchBoxCleaned = () => {
if (vm.cleanByFilters) {
vm.cleanByFilters = false;
} else {
vm.invalidParams = false;
vm.filterTxs(vm.lastDirection);
vm.txs.loadData();
}
};

const searchByParams = (params) => {
if (vm.direction !== 'search') {
vm.lastDirection = vm.direction;
vm.direction = 'search';
}
vm.invalidParams = false;
vm.txs = addressTxs(params);
vm.txs.loadData();
};

const isValidAddress = id => /([0-9]+)L$/.test(id);

const onSearchChange = () => {
const params = {};
Object.keys(vm.searchModel).forEach((key) => {
if (vm.searchModel[key] !== undefined && vm.searchModel[key] !== '') {
params[key] = vm.searchModel[key];
}
if ((key === 'minAmount' || key === 'maxAmount') && params[key] !== '') {
params[key] = Math.floor(parseFloat(params[key]) * 1e8);
}
});

if (params.query) {
params.senderId = params.query;
params.recipientId = params.query;
} else {
params.senderId = params.senderId || $stateParams.address;
params.recipientId = params.recipientId || $stateParams.address;
}

if (Object.keys(params).length > 0 &&
(isValidAddress(params.recipientId) ||
isValidAddress(params.senderId))) {
searchByParams(params);
} else if (Object.keys(vm.searchModel).length === 0) {
onSearchBoxCleaned();
} else {
vm.invalidParams = true;
}
};

$rootScope.$on('advanced-searchbox:modelUpdated', (event, model) => {
if (vm.searchModel.query !== model.query) {
vm.searchModel = Object.assign({}, model);
return onSearchChange();
}

return vm.searchModel = Object.assign({}, model);
});

$rootScope.$on('advanced-searchbox:removedSearchParam', (event, searchParameter) => {
delete vm.searchModel[searchParameter.key];
onSearchChange();
});

$rootScope.$on('advanced-searchbox:removedAllSearchParam', () => {
onSearchBoxCleaned();
});

$rootScope.$on('advanced-searchbox:leavedEditMode', () => {
onSearchChange();
});

vm.getAddress();
vm.txs = addressTxs({ address: $stateParams.address });
};
Expand Down
14 changes: 1 addition & 13 deletions src/components/address/address.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,19 +112,7 @@ <h1>

<div data-ng-if="vm.address.balance" data-ng-init="vm.txs.loadData()">
<h1>Transactions</h1>
<section class='horizontal-padding-xs horizontal-padding-s horizontal-padding-m horizontal-padding-l double'>
<nit-advanced-searchbox
ng-model="vm.searchParams"
parameters="vm.availableSearchParams"
parameters-display-limit="vm.parametersDisplayLimit"
placeholder="Search...">
</nit-advanced-searchbox>
<div ng-if="vm.invalidParams" class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<span class="sr-only">Error:</span>
Please provide a valid address for sender id and/or recipient id. Username is not accepted.
</div>
</section>
<transactions-filter data-txs="vm.txs" data-address="vm.address.address"></transactions-filter>
<div class="transactions-filter-header horizontal-padding-xs horizontal-padding-s horizontal-padding-m horizontal-padding-l double" role="group"
aria-label="All/Sent/Received Transactions" data-ng-class="{disabled: vm.direction === 'search'}">
<button data-ng-disabled="!vm.direction" data-ng-click="vm.filterTxs();vm.txs.loadData();vm.onFiltersUsed();">All</button>
Expand Down
5 changes: 5 additions & 0 deletions src/components/home/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ <h1>Latest Transactions</h1>
</tr>
</tbody>
</table>
<div class="paginator">
<div class="btn-group" role="group">
<a href="/txs/" class="btn btn-default see-all-transactions bordered-button">See all transactions</a>
</div>
</div>
</div>

<h1>Latest Blocks</h1>
Expand Down
1 change: 1 addition & 0 deletions src/components/transactions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
*/
import './transactions.module';
import './transactions.component';
import './transaction.component';
45 changes: 45 additions & 0 deletions src/components/transactions/transaction.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* LiskHQ/lisk-explorer
* Copyright © 2018 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*
*/
import AppTransaction from './transactions.module';
import template from './transaction.html';

const TransactionConstructor = function ($rootScope, $stateParams, $location, $http) {
const vm = this;
vm.getTransaction = () => {
$http.get('/api/getTransaction', {
params: {
transactionId: $stateParams.txId,
},
}).then((resp) => {
if (resp.data.success) {
vm.tx = resp.data.transaction;
} else {
throw new Error('Transaction was not found!');
}
}).catch(() => {
$location.path('/');
});
};

vm.getTransaction();
};

AppTransaction.component('transaction', {
template,
controller: TransactionConstructor,
controllerAs: 'vm',
});

Loading