fix(embark-ui): correctly calculate which transactions to display

Revise calculations related to transactions and pagination in the transactions
explorer and explorers overview.

Make the number of transactions to display per page configurable and set it to
3 in the explorers overview. Display a "No transactions..." message instead of
"0" when there are no transactions to display.

Introduce a `blocksFull` prop, action, api, etc. for calculating lists of
transactions relative to block objects that contain full transaction objects
instead of hash strings (the function backing the blocks endpoint already
supports that) and transaction receipts. In the future, the receipts can be
used to filter out constructor transactions for silent contracts.

Make pagination display conditional within the blocks explorer, same as in the
transactions explorer.
This commit is contained in:
Michael Bradley, Jr 2019-03-14 16:36:10 -05:00 committed by Michael Bradley
parent b1881719e0
commit fbeea47a6e
9 changed files with 135 additions and 47 deletions

View File

@ -82,6 +82,15 @@ export const blocks = {
failure: (error) => action(BLOCKS[FAILURE], {error})
};
export const BLOCKS_FULL = createRequestTypes('BLOCKS_FULL');
export const blocksFull = {
request: (from, limit) => {
return action(BLOCKS_FULL[REQUEST], {from, limit, txObjects: true, txReceipts: true});
},
success: (blocksFull) => action(BLOCKS_FULL[SUCCESS], {blocksFull}),
failure: (error) => action(BLOCKS_FULL[FAILURE], {error})
};
export const BLOCK = createRequestTypes('BLOCK');
export const block = {
request: (blockNumber) => action(BLOCK[REQUEST], {blockNumber}),

View File

@ -38,7 +38,7 @@ const Blocks = ({blocks, changePage, currentPage, numberOfPages}) => (
</Row>
</div>
))}
<Pagination changePage={changePage} currentPage={currentPage} numberOfPages={numberOfPages}/>
{numberOfPages > 0 && <Pagination changePage={changePage} currentPage={currentPage} numberOfPages={numberOfPages}/>}
</CardBody>
</Card>
</Col>

View File

@ -25,7 +25,7 @@ const ExplorerDashboardLayout = () => (
<BlocksContainer numBlocksToDisplay={5} overridePageHead={false} />
</Col>
<Col xl={6}>
<TransactionsContainer overridePageHead={false} />
<TransactionsContainer numTxsToDisplay={3} overridePageHead={false} />
</Col>
</Row>
</div>

View File

@ -15,6 +15,7 @@ const Transactions = ({transactions, contracts, changePage, currentPage, numberO
<h2>Transactions</h2>
</CardHeader>
<CardBody>
{!transactions.length && "No transactions to display"}
{transactions.map(transaction => (
<div className="explorer-row border-top" key={transaction.hash}>
<CardTitleIdenticon id={transaction.hash}>Transaction&nbsp;
@ -47,7 +48,7 @@ const Transactions = ({transactions, contracts, changePage, currentPage, numberO
</Row>
</div>
))}
{numberOfPages && <Pagination changePage={changePage} currentPage={currentPage} numberOfPages={numberOfPages}/>}
{numberOfPages > 0 && <Pagination changePage={changePage} currentPage={currentPage} numberOfPages={numberOfPages}/>}
</CardBody>
</Card>
</Col>

View File

@ -1,12 +1,14 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {transactions as transactionsAction, initBlockHeader, stopBlockHeader, contracts as contractsAction} from '../actions';
import {blocksFull as blocksAction,
contracts as contractsAction,
initBlockHeader,
stopBlockHeader} from '../actions';
import Transactions from '../components/Transactions';
import DataWrapper from "../components/DataWrapper";
import PageHead from "../components/PageHead";
import {getTransactions, getContracts} from "../reducers/selectors";
import {getBlocksFull, getContracts} from "../reducers/selectors";
const MAX_TXS = 10; // TODO use same constant as API
@ -14,13 +16,13 @@ class TransactionsContainer extends Component {
constructor(props) {
super(props);
this.state = {currentPage: 0};
this.numberOfTxs = 0;
this.currentTxs = [];
this.numTxsToDisplay = this.props.numTxsToDisplay || MAX_TXS;
this.numBlocksToFetch = this.numTxsToDisplay;
this.state = {currentPage: 1};
}
componentDidMount() {
this.props.fetchTransactions();
this.props.fetchBlocksFull(null, this.numBlocksToFetch);
this.props.fetchContracts();
this.props.initBlockHeader();
}
@ -29,44 +31,78 @@ class TransactionsContainer extends Component {
this.props.stopBlockHeader();
}
get numberOfBlocks() {
const blocks = this.props.blocks;
return !blocks.length ? 0 : blocks[0].number + 1;
}
get estNumberOfTxs() {
const blocks = this.props.blocks;
const numBlocksInProps = blocks.length;
const numTxsInPropsBlocks = blocks.reduce((txCount, block) => (
txCount + block.transactions.length
), 0);
const missingNumBlocks = this.numberOfBlocks - numBlocksInProps;
return missingNumBlocks + numTxsInPropsBlocks;
}
getNumberOfPages() {
if (!this.numberOfTxs) {
let transactions = this.props.transactions;
if (transactions.length === 0) {
this.numberOfTxs = 0;
} else {
this.numberOfTxs = transactions[transactions.length - 1].blockNumber - 1;
}
}
return Math.ceil(this.numberOfTxs / MAX_TXS);
return Math.ceil(this.estNumberOfTxs / this.numTxsToDisplay);
}
changePage(newPage) {
this.setState({currentPage: newPage});
this.props.fetchTransactions((newPage * MAX_TXS) + MAX_TXS);
this.props.fetchBlocksFull(
this.numberOfBlocks - 1 - (this.numBlocksToFetch * (newPage - 1)),
this.numBlocksToFetch
);
}
getCurrentTransactions() {
const currentPage = this.state.currentPage || this.getNumberOfPages();
return this.props.transactions.filter(tx => tx.blockNumber <= (currentPage * MAX_TXS) + MAX_TXS &&
tx.blockNumber > currentPage * MAX_TXS);
if (!this.props.blocks.length) return [];
let relativeBlock = this.numberOfBlocks - 1;
let offset = 0;
let txs = this.props.blocks.reduce((txs, block) => {
offset = relativeBlock - block.number;
if (offset <= 1) {
offset = 0;
}
relativeBlock = block.number;
const txsLength = txs.length;
block.transactions.forEach((tx, idx) => {
txs[txsLength + idx + offset] = tx;
});
return txs;
}, []);
const estNumberOfTxs = this.estNumberOfTxs;
return txs.filter((tx, idx) => {
const txNumber = estNumberOfTxs - idx;
const index = (
(estNumberOfTxs -
(this.numTxsToDisplay * (this.state.currentPage - 1))) -
txNumber + 1
);
return index <= this.numTxsToDisplay && index > 0;
});
}
render() {
const newTxs = this.getCurrentTransactions();
if (newTxs.length) {
this.currentTxs = newTxs;
}
return (
<React.Fragment>
<PageHead title="Transactions" enabled={this.props.overridePageHead} description="Summary view of all transactions occurring on the node configured for Embark" />
<DataWrapper shouldRender={this.currentTxs.length > 0} {...this.props} render={() => (
<Transactions transactions={this.currentTxs}
<PageHead
title="Transactions"
enabled={this.props.overridePageHead}
description="Summary view of all transactions occurring on the node configured for Embark" />
<DataWrapper
shouldRender={true}
{...this.props}
render={() => (
<Transactions transactions={newTxs}
contracts={this.props.contracts}
numberOfPages={this.getNumberOfPages()}
changePage={(newPage) => this.changePage(newPage)}
currentPage={this.state.currentPage || this.getNumberOfPages()} />
currentPage={this.state.currentPage} />
)} />
</React.Fragment>
);
@ -75,7 +111,7 @@ class TransactionsContainer extends Component {
function mapStateToProps(state) {
return {
transactions: getTransactions(state),
blocks: getBlocksFull(state),
contracts: getContracts(state),
error: state.errorMessage,
loading: state.loading
@ -83,9 +119,9 @@ function mapStateToProps(state) {
}
TransactionsContainer.propTypes = {
transactions: PropTypes.arrayOf(PropTypes.object),
blocks: PropTypes.arrayOf(PropTypes.object),
contracts: PropTypes.arrayOf(PropTypes.object),
fetchTransactions: PropTypes.func,
fetchBlocksFull: PropTypes.func,
fetchContracts: PropTypes.func,
initBlockHeader: PropTypes.func,
stopBlockHeader: PropTypes.func,
@ -97,7 +133,7 @@ TransactionsContainer.propTypes = {
export default connect(
mapStateToProps,
{
fetchTransactions: transactionsAction.request,
fetchBlocksFull: blocksAction.request,
fetchContracts: contractsAction.request,
initBlockHeader,
stopBlockHeader

View File

@ -18,6 +18,7 @@ const PROCESS_LOGS_LIMIT = ELEMENTS_LIMIT * 2;
const entitiesDefaultState = {
accounts: [],
blocks: [],
blocksFull: [],
transactions: [],
processes: [],
services: [],
@ -43,6 +44,9 @@ const sorter = {
blocks: function(a, b) {
return b.number - a.number;
},
blocksFull: function(a, b) {
return b.number - a.number;
},
transactions: function(a, b) {
return ((BN_FACTOR * b.blockNumber) + b.transactionIndex) - ((BN_FACTOR * a.blockNumber) + a.transactionIndex);
},
@ -120,6 +124,13 @@ const filters = {
return index === self.findIndex((t) => t.number === block.number);
},
blocksFull: function(block, index, self) {
if (index > ELEMENTS_LIMIT) {
return false;
}
return index === self.findIndex((t) => t.number === block.number);
},
transactions: function(tx, index, self) {
if (index > ELEMENTS_LIMIT) {
return false;

View File

@ -44,6 +44,10 @@ export function getBlocks(state) {
return state.entities.blocks;
}
export function getBlocksFull(state) {
return state.entities.blocksFull;
}
export function getLastBlock(state) {
return state.entities.blocks[0];
}

View File

@ -47,6 +47,7 @@ export const fetchBlock = doRequest.bind(null, actions.block, api.fetchBlock);
export const fetchTransaction = doRequest.bind(null, actions.transaction, api.fetchTransaction);
export const fetchAccounts = doRequest.bind(null, actions.accounts, api.fetchAccounts);
export const fetchBlocks = doRequest.bind(null, actions.blocks, api.fetchBlocks);
export const fetchBlocksFull = doRequest.bind(null, actions.blocksFull, api.fetchBlocks);
export const fetchTransactions = doRequest.bind(null, actions.transactions, api.fetchTransactions);
export const fetchProcesses = doRequest.bind(null, actions.processes, api.fetchProcesses);
export const fetchServices = doRequest.bind(null, actions.services, api.fetchServices);
@ -120,6 +121,10 @@ export function *watchFetchBlocks() {
yield takeEvery(actions.BLOCKS[actions.REQUEST], fetchBlocks);
}
export function *watchFetchBlocksFull() {
yield takeEvery(actions.BLOCKS_FULL[actions.REQUEST], fetchBlocksFull);
}
export function *watchFetchAccount() {
yield takeEvery(actions.ACCOUNT[actions.REQUEST], fetchAccount);
}
@ -389,6 +394,7 @@ export function *initBlockHeader() {
return;
}
yield put({type: actions.BLOCKS[actions.REQUEST]});
yield put({type: actions.BLOCKS_FULL[actions.REQUEST], txObjects: true, txReceipts: true});
yield put({type: actions.TRANSACTIONS[actions.REQUEST]});
}
}
@ -568,6 +574,7 @@ export default function *root() {
fork(watchFetchVersions),
fork(watchFetchPlugins),
fork(watchFetchBlocks),
fork(watchFetchBlocksFull),
fork(watchFetchContracts),
fork(watchFetchContractProfile),
fork(watchPostContractFunction),

View File

@ -387,9 +387,9 @@ class BlockchainConnector {
'get',
'/embark-api/blockchain/blocks',
(req, res) => {
let from = parseInt(req.query.from, 10);
let limit = req.query.limit || 10;
self.getBlocks(from, limit, false, res.send.bind(res));
const from = parseInt(req.query.from, 10);
const limit = req.query.limit || 10;
self.getBlocks(from, limit, !!req.query.txObjects, !!req.query.txReceipts, res.send.bind(res));
}
);
@ -565,7 +565,7 @@ class BlockchainConnector {
}
getTransactions(blockFrom, blockLimit, callback) {
this.getBlocks(blockFrom, blockLimit, true, (blocks) => {
this.getBlocks(blockFrom, blockLimit, true, true, (blocks) => {
let transactions = blocks.reduce((acc, block) => {
if (!block || !block.transactions) {
return acc;
@ -576,7 +576,7 @@ class BlockchainConnector {
});
}
getBlocks(from, limit, returnTransactionObjects, callback) {
getBlocks(from, limit, returnTransactionObjects, includeTransactionReceipts, callback) {
let self = this;
let blocks = [];
async.waterfall([
@ -597,12 +597,32 @@ class BlockchainConnector {
function(next) {
async.times(limit, function(n, eachCb) {
self.web3.eth.getBlock(from - n, returnTransactionObjects, function(err, block) {
if (err && err.message) {
if (err) {
// FIXME Returns an error because we are too low
return eachCb();
}
if (!block) {
return eachCb();
}
if (!(returnTransactionObjects && includeTransactionReceipts) ||
!(block.transactions && block.transactions.length)) {
blocks.push(block);
return eachCb();
}
return Promise.all(block.transactions.map(tx => (
self.web3.eth.getTransactionReceipt(tx.hash)
)))
.then(receipts => {
block.transactions.forEach((tx, index) => {
tx['receipt'] = receipts[index];
});
blocks.push(block);
eachCb();
})
.catch((err) => {
self.logger.error(err.message || err);
eachCb();
});
});
}, next);
}