diff --git a/lib/api/transactions.js b/lib/api/transactions.js
index 3de473072..01d2e9e7d 100644
--- a/lib/api/transactions.js
+++ b/lib/api/transactions.js
@@ -21,6 +21,7 @@ const coreUtils = require('../../utils/core.js');
module.exports = function (app) {
const self = this;
const knowledge = app.knownAddresses;
+ const invalidAddress = '__invalid_address';
const param = (p, d) => {
p = parseInt(p, 10);
@@ -263,13 +264,26 @@ module.exports = function (app) {
});
};
- const normalizeTransactionParams = (params) => {
+ const parseLiskAddress = async (address) => {
+ const liskIdPattern = /^[0-9]{1,21}[L|l]$/g;
+
+ if (!address || typeof address !== 'string') return '';
+ if (address.match(liskIdPattern)) return coreUtils.parseAddress(address);
+ address = await knowledge.getByUser(address);
+ if (typeof address === 'object' && typeof address.address === 'string') return address.address;
+ return invalidAddress;
+ };
+
+ const normalizeTransactionParams = async (params) => {
const directionQueries = [];
const offset = param(params.offset, 0);
const limit = param(params.limit, 100);
const sort = params.sort || 'timestamp:desc';
- const address = coreUtils.parseAddress(params.address);
+
+ const address = await parseLiskAddress(params.address);
+ params.senderId = await parseLiskAddress(params.senderId);
+ params.recipientId = await parseLiskAddress(params.recipientId);
let baseQuery = `sort=${sort}&offset=${offset}&limit=${limit}`;
@@ -295,11 +309,13 @@ module.exports = function (app) {
filters = filters.filter(item => item !== 'senderId' && item !== 'recipientId');
}
- Object.keys(params).forEach((key) => {
- if ((filters.indexOf(key) >= 0)) {
- baseQuery += `&${key}=${params[key]}`;
- }
- });
+ Object.keys(params)
+ .filter(p => !!params[p])
+ .forEach((key) => {
+ if ((filters.indexOf(key) >= 0)) {
+ baseQuery += `&${key}=${params[key]}`;
+ }
+ });
// type might be comma separate or undefined
if (params.type) {
@@ -352,16 +368,16 @@ module.exports = function (app) {
});
};
- this.getTransactions = function (query, error, success) {
- const queryList = normalizeTransactionParams(query);
+ this.getTransactions = async function (query, error, success) {
+ const queryList = await normalizeTransactionParams(query);
this.getTransactionsCall(queryList, error, success);
};
- this.getTransactionsByAddress = function (query, error, success) {
+ this.getTransactionsByAddress = async 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);
+ const queryList = await normalizeTransactionParams(query);
return this.getTransactionsCall(queryList, error, success);
};
diff --git a/src/app/states.js b/src/app/states.js
index 7907dafb4..d2012bdd0 100644
--- a/src/app/states.js
+++ b/src/app/states.js
@@ -33,7 +33,7 @@ App.config(($stateProvider, $urlRouterProvider, $locationProvider) => {
component: 'block',
})
.state('transactions', {
- url: '/txs/:page',
+ url: '/txs/:page?senderId&senderPublicKey&recipientId&recipientPublicKey&minAmount&maxAmount&type&height&blockId&fromTimestamp&toTimestamp&sort&limit&offset',
parentDir: 'home',
component: 'transactions',
})
diff --git a/src/assets/styles/common.css b/src/assets/styles/common.css
index 9bd15268e..b96429e79 100644
--- a/src/assets/styles/common.css
+++ b/src/assets/styles/common.css
@@ -1605,3 +1605,32 @@ li.dropdown.disabled a {
#footer .social-networks {
padding-top: 5px;
}
+
+.transaction-filter button {
+ display: inline-block;
+ padding: 6px 12px;
+ margin-bottom: 0;
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 1.42857143;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ touch-action: manipulation;
+ cursor: pointer;
+ user-select: none;
+ background-image: none;
+ border: 1px solid #ccc;
+ border-style: solid none;
+ height: auto;
+ width: auto;
+}
+
+.transaction-filter button:first-of-type {
+ border-style: solid none solid none;
+}
+
+.transaction-filter button:last-of-type {
+ border-style: solid solid solid none;
+}
+
diff --git a/src/components/address/address.html b/src/components/address/address.html
index 766820b97..0dee48da6 100644
--- a/src/components/address/address.html
+++ b/src/components/address/address.html
@@ -108,7 +108,7 @@
-
+
diff --git a/src/components/transactions/transactions.component.js b/src/components/transactions/transactions.component.js
index 81770db12..7e76d018a 100644
--- a/src/components/transactions/transactions.component.js
+++ b/src/components/transactions/transactions.component.js
@@ -18,21 +18,36 @@ import template from './transactions.html';
const TransactionsConstructor = function ($rootScope, $stateParams, $state, $http, $interval) {
const vm = this;
+
+ const filters = Object.keys($stateParams)
+ .filter(key => key !== 'page')
+ .filter(key => key !== '#')
+ .filter(key => typeof $stateParams[key] !== 'undefined')
+ .map(key => `${key}=${$stateParams[key]}`);
+
vm.getLastTransactions = (n) => {
const limit = 20 + 1;
let offset = 0;
if (n) offset = (n - 1) * limit;
- $http.get(`/api/getTransactions?limit=${limit}&offset=${offset}`).then((resp) => {
+ let requestUrl = `/api/getTransactions?limit=${limit}&offset=${offset}`;
+ requestUrl += filters.length ? `&${filters.join('&')}` : '';
+
+ $http.get(requestUrl).then((resp) => {
if (resp.data.success) {
- const removedTx = resp.data.transactions.splice(-1, 1);
+ let removedTx;
+ if (resp.data.transactions.length > limit - 1) {
+ removedTx = resp.data.transactions.splice(-1, 1);
+ }
vm.txs = { results: resp.data.transactions };
vm.txs.hasPrev = !!offset;
vm.txs.hasNext = !!removedTx;
- vm.txs.page = $stateParams.page || 0;
+ vm.txs.page = $stateParams.page || 1;
vm.txs.loadPageOffset = vm.loadPageOffset;
vm.txs.loadPage = vm.loadPage;
+ vm.txs.activeSort = vm.activeSort;
+ vm.txs.applySort = vm.applySort;
} else {
vm.txs = {};
}
@@ -47,6 +62,15 @@ const TransactionsConstructor = function ($rootScope, $stateParams, $state, $htt
$state.go($state.current.component, { page: pageNumber });
};
+ vm.applySort = (predicate) => {
+ const direction = (predicate === vm.activeSort.predicate && vm.activeSort.direction === 'asc') ? 'desc' : 'asc';
+ $state.go($state.current.component, { sort: `${predicate}:${direction}` });
+ };
+
+ vm.activeSort = typeof $stateParams.sort === 'string'
+ ? { predicate: $stateParams.sort.split(':')[0], direction: $stateParams.sort.split(':')[1] }
+ : { predicate: 'timestamp', direction: 'desc' };
+
const update = () => {
vm.getLastTransactions($stateParams.page || 1);
};
diff --git a/src/components/transactions/transactions.html b/src/components/transactions/transactions.html
index 56b1940d3..a6dd7da12 100644
--- a/src/components/transactions/transactions.html
+++ b/src/components/transactions/transactions.html
@@ -17,13 +17,13 @@
-->
Transactions
+
+
+
+
Loading transactions list
-
-
-
-
diff --git a/src/services/pagination.js b/src/services/pagination.js
index cce960d37..305ad61d9 100644
--- a/src/services/pagination.js
+++ b/src/services/pagination.js
@@ -25,6 +25,7 @@ const Pagination = function ($http, $q, params) {
this.key = params.key || '';
this.offset = Number(params.offset) || 0;
this.currentPage = Number(params.currentPage) || 1;
+ this.page = this.currentPage;
this.limit = Number(params.limit) || 25;
['url', 'parent', 'key', 'offset', 'limit'].forEach((key) => {
diff --git a/src/shared/transactions-filter/transactions-filter.directive.js b/src/shared/transactions-filter/transactions-filter.directive.js
index 863c93955..1ab23775f 100644
--- a/src/shared/transactions-filter/transactions-filter.directive.js
+++ b/src/shared/transactions-filter/transactions-filter.directive.js
@@ -16,108 +16,78 @@
import AppTools from '../../app/app-tools.module';
import template from './transactions-filter.html';
-const TransactionsConstructor = function ($rootScope, $scope, $timeout, allTxs) {
+const TransactionsConstructor = function ($rootScope, $scope, $stateParams, $element, $state) {
$scope.searchModel = [];
$scope.searchParams = [];
$scope.availableSearchParams = [
- { key: 'senderId', name: 'Sender ID', placeholder: 'Sender...' },
- { key: 'senderPublicKey', name: 'Sender Public Key', placeholder: 'Sender Public Key...' },
- { key: 'recipientId', name: 'Recipient ID', placeholder: 'Recipient...' },
- { key: 'recipientPublicKey', name: 'Recipient Public Key', placeholder: 'Recipient Public Key...' },
- { key: 'minAmount', name: 'Min Amount', placeholder: 'Min Amount...' },
- { key: 'maxAmount', name: 'Max Amount', placeholder: 'Max Amount...' },
- { key: 'type', name: 'Transaction Type', placeholder: 'Comma separated...' },
- { key: 'height', name: 'Block Height', placeholder: 'Block Height...' },
- { key: 'blockId', name: 'Block Id', placeholder: 'Block Id...' },
- { key: 'fromTimestamp', name: 'From Timestamp', placeholder: 'From Timestamp...' },
- { key: 'toTimestamp', name: 'To Timestamp', placeholder: 'To Timestamp...' },
- { key: 'limit', name: 'Limit', placeholder: 'Limit...' },
- { key: 'offset', name: 'Offset', placeholder: 'Offset...' },
- {
- key: 'sort',
- name: 'Order By',
- placeholder: 'Order By...',
- restrictToSuggestedValues: true,
- suggestedValues: ['amount:asc', 'amount:desc', 'fee:asc', 'fee:desc', 'type:asc', 'type:desc', 'timestamp:asc', 'timestamp:desc'],
- },
+ { key: 'senderId', name: 'Sender ID', placeholder: 'Sender...', example: '12317412804123L' },
+ { key: 'senderPublicKey', name: 'Sender Public Key', placeholder: 'Sender Public Key...', example: 'b550ede5...a26c78d8' },
+ { key: 'recipientId', name: 'Recipient ID', placeholder: 'Recipient...', example: '12317412804123L' },
+ { key: 'recipientPublicKey', name: 'Recipient Public Key', placeholder: 'Recipient Public Key...', example: 'b550ede5...a26c78d8' },
+ { key: 'minAmount', name: 'Min Amount', placeholder: 'Min Amount...', example: '1.25' },
+ { key: 'maxAmount', name: 'Max Amount', placeholder: 'Max Amount...', example: '1000.5' },
+ { key: 'type', name: 'Comma separated transaction types', placeholder: 'Comma separated...', example: '1,3' },
+ { key: 'height', name: 'Block height', placeholder: 'Block Height...', example: '2963014' },
+ { key: 'blockId', name: 'Block Id', placeholder: 'Block Id...', example: '17238091754034756025' },
+ { key: 'fromTimestamp', name: 'From Timestamp', placeholder: 'From Timestamp...', example: '17238091754034756025' },
+ { key: 'toTimestamp', name: 'To Timestamp', placeholder: 'To Timestamp...', example: '12317412804123L' },
+ // { key: 'limit', name: 'Limit', placeholder: 'Limit...', example: '12317412804123L' },
+ // { key: 'offset', name: 'Offset', placeholder: 'Offset...', example: '12317412804123L' },
+ // {
+ // key: 'sort',
+ // name: 'Order By',
+ // placeholder: 'Order By...',
+ // restrictToSuggestedValues: true,
+ // suggestedValues:
+ // ['amount:asc', 'amount:desc', 'fee:asc', 'fee:desc', 'type:asc',
+ // 'type:desc', 'timestamp:asc', 'timestamp:desc'],
+ // },
];
- $scope.parametersDisplayLimit = $scope.availableSearchParams.length;
-
- const onSearchBoxCleaned = () => {
- if ($scope.cleanByFilters) {
- $scope.cleanByFilters = false;
- } else {
- $scope.invalidParams = false;
- $scope.txs = allTxs();
- $scope.txs.loadData();
+ const convertToUrl = (key, param) => {
+ switch (key) {
+ case 'minAmount':
+ case 'maxAmount':
+ return Number(param) * Math.pow(10, 8);
+ default:
+ return param;
}
};
- const searchByParams = (params) => {
- if ($scope.direction !== 'search') {
- $scope.lastDirection = $scope.direction;
- $scope.direction = 'search';
+ const convertFromUrl = (key, param) => {
+ switch (key) {
+ case 'minAmount':
+ case 'maxAmount':
+ return Number(param) / Math.pow(10, 8);
+ default:
+ return encodeURI(param);
}
- $scope.invalidParams = false;
- $scope.txs = allTxs(params);
- $scope.txs.loadData();
};
- const onSearchChange = () => {
- const params = {};
- Object.keys($scope.searchModel).forEach((key) => {
- if ($scope.searchModel[key] !== undefined && $scope.searchModel[key] !== '') {
- params[key] = $scope.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;
- }
-
- if (Object.keys(params).length > 0) {
- searchByParams(params);
- } else {
- onSearchBoxCleaned();
- }
+ $scope.addToQueryText = (key) => {
+ $scope.queryText += ` ${key}=`;
};
- $rootScope.$on('advanced-searchbox:modelUpdated', (event, model) => {
- if ($scope.searchModel.query !== model.query) {
- $scope.searchModel = Object.assign({}, model);
- return onSearchChange();
- }
-
- return $scope.searchModel = Object.assign({}, model);
- });
-
- $rootScope.$on('advanced-searchbox:removedSearchParam', (event, searchParameter) => {
- delete $scope.searchModel[searchParameter.key];
- onSearchChange();
- });
-
- $rootScope.$on('advanced-searchbox:removedAllSearchParam', () => {
- onSearchBoxCleaned();
- });
-
- $rootScope.$on('advanced-searchbox:leavedEditMode', (event, searchParameter) => {
- $scope.searchModel[searchParameter.key] = searchParameter.value;
- onSearchChange();
- });
-
- // Sets autocomplete attr off
- const disableAutocomplete = () => {
- $timeout(() => {
- document.getElementsByClassName('search-parameter-input')[0].setAttribute('autocomplete', 'off');
- }, 0);
+ $scope.performSearch = () => {
+ const query = $scope.queryText.split(' ')
+ .map(param => param.split('='))
+ .reduce((acc, param) => {
+ acc[param[0]] = convertToUrl(param[0], param[1]);
+ return acc;
+ }, {});
+ $state.go($state.current.component, Object.assign({ page: 1 }, query), { inherit: false });
};
- disableAutocomplete();
+ $scope.queryText = Object.keys($stateParams)
+ .filter(key => key !== 'page')
+ .filter(key => key !== 'address')
+ .filter(key => key !== 'sort')
+ .filter(key => key !== '#')
+ .filter(key => typeof $stateParams[key] !== 'undefined')
+ .map(key => `${key}=${convertFromUrl(key, $stateParams[key])}`)
+ .join(' ');
+
+ $scope.parametersDisplayLimit = $scope.availableSearchParams.length;
};
const transactionsFilter = AppTools.directive('transactionsFilter', () => ({
diff --git a/src/shared/transactions-filter/transactions-filter.html b/src/shared/transactions-filter/transactions-filter.html
index 2aecf281f..9d72c97cc 100644
--- a/src/shared/transactions-filter/transactions-filter.html
+++ b/src/shared/transactions-filter/transactions-filter.html
@@ -15,16 +15,53 @@
*
*/
-->
-
-
-
-
-
-
Error:
- Please provide a valid address for sender id and/or recipient id. Username is not accepted.
+
+
+
+
+
Close [x]
+
Available params
+
{{ param.key }},
+
Transaction types
+
0: Balance transfer, 1: Second signature creation, 2: Delegate registration, 3: Delegate vote, 4: Multi-signature creation, 5: DApp registration
+
Examples
+
{{ availableSearchParams[i].key }}={{ availableSearchParams[i].example }}
+
{{ availableSearchParams[i].key }}={{ availableSearchParams[i].example }}
+
+
For detailed instruction click here...
+
+
+
+
+
Close [x]
+
+
+ Param |
+ Name |
+ Example |
+
+
+
+ {{ param.key }} |
+ {{ param.name }} |
+ {{ param.example }} |
+
+
+
+
diff --git a/src/shared/transactions-list/transactions-list.html b/src/shared/transactions-list/transactions-list.html
index 8f2794560..58b46b8aa 100644
--- a/src/shared/transactions-list/transactions-list.html
+++ b/src/shared/transactions-list/transactions-list.html
@@ -15,42 +15,31 @@
*
*/
-->
-
-
-
-
No transactions
-
There are no transactions involving this {{txs.parent}}.
-
-
-
-
-
- Transaction ID
-
-
+ |
+ Transaction ID
ID
|
-
- Date
+ |
+ Date
|
-
- Sender
+ |
+ Sender
|
-
- Recipient
+ |
+ Recipient
|
-
- Amount
+ |
+ Amount
|
-
- Fee
+ |
+ Fee
|
-
- Confirm.
+ |
+ Confirm.
|
@@ -66,7 +55,7 @@ No transactions
-
+
@@ -81,3 +70,11 @@ No transactions
+
+
+
+
No transactions
+
There are no transactions found.
+
+
+
diff --git a/utils/knownAddresses.js b/utils/knownAddresses.js
index 33c03bab2..f77db7453 100644
--- a/utils/knownAddresses.js
+++ b/utils/knownAddresses.js
@@ -44,11 +44,30 @@ module.exports = function (app, config, client) {
return false;
}
- return client.hmset(`address:${account.address}`, entry);
+ const writeToHmset = (key, value) => {
+ client.hmset(key, value, (err) => {
+ if (err) logger.error(err.message);
+ });
+ };
+
+ writeToHmset(`username:${entry.owner}`, { address: account.address });
+ writeToHmset(`address:${account.address}`, entry);
+
+ return true;
};
this.getKnownAddress = (address, callback) => client.hgetall(`address:${address}`, callback);
+ const getFromHmset = key => new Promise((resolve, reject) => {
+ client.hgetall(key, (err, result) => {
+ if (err) reject(err.message);
+ resolve(result || {});
+ });
+ });
+
+ this.getByAddress = async address => getFromHmset(`address:${address}`);
+ this.getByUser = async username => getFromHmset(`username:${username}`);
+
this.loadFromJson = () => {
try {
logger.info('KnownAddresses:', 'Loading known addresses...');