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}) 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 = createRequestTypes('BLOCK');
export const block = { export const block = {
request: (blockNumber) => action(BLOCK[REQUEST], {blockNumber}), request: (blockNumber) => action(BLOCK[REQUEST], {blockNumber}),

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ const PROCESS_LOGS_LIMIT = ELEMENTS_LIMIT * 2;
const entitiesDefaultState = { const entitiesDefaultState = {
accounts: [], accounts: [],
blocks: [], blocks: [],
blocksFull: [],
transactions: [], transactions: [],
processes: [], processes: [],
services: [], services: [],
@ -43,6 +44,9 @@ const sorter = {
blocks: function(a, b) { blocks: function(a, b) {
return b.number - a.number; return b.number - a.number;
}, },
blocksFull: function(a, b) {
return b.number - a.number;
},
transactions: function(a, b) { transactions: function(a, b) {
return ((BN_FACTOR * b.blockNumber) + b.transactionIndex) - ((BN_FACTOR * a.blockNumber) + a.transactionIndex); 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); 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) { transactions: function(tx, index, self) {
if (index > ELEMENTS_LIMIT) { if (index > ELEMENTS_LIMIT) {
return false; return false;

View File

@ -44,6 +44,10 @@ export function getBlocks(state) {
return state.entities.blocks; return state.entities.blocks;
} }
export function getBlocksFull(state) {
return state.entities.blocksFull;
}
export function getLastBlock(state) { export function getLastBlock(state) {
return state.entities.blocks[0]; 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 fetchTransaction = doRequest.bind(null, actions.transaction, api.fetchTransaction);
export const fetchAccounts = doRequest.bind(null, actions.accounts, api.fetchAccounts); export const fetchAccounts = doRequest.bind(null, actions.accounts, api.fetchAccounts);
export const fetchBlocks = doRequest.bind(null, actions.blocks, api.fetchBlocks); 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 fetchTransactions = doRequest.bind(null, actions.transactions, api.fetchTransactions);
export const fetchProcesses = doRequest.bind(null, actions.processes, api.fetchProcesses); export const fetchProcesses = doRequest.bind(null, actions.processes, api.fetchProcesses);
export const fetchServices = doRequest.bind(null, actions.services, api.fetchServices); export const fetchServices = doRequest.bind(null, actions.services, api.fetchServices);
@ -120,6 +121,10 @@ export function *watchFetchBlocks() {
yield takeEvery(actions.BLOCKS[actions.REQUEST], fetchBlocks); yield takeEvery(actions.BLOCKS[actions.REQUEST], fetchBlocks);
} }
export function *watchFetchBlocksFull() {
yield takeEvery(actions.BLOCKS_FULL[actions.REQUEST], fetchBlocksFull);
}
export function *watchFetchAccount() { export function *watchFetchAccount() {
yield takeEvery(actions.ACCOUNT[actions.REQUEST], fetchAccount); yield takeEvery(actions.ACCOUNT[actions.REQUEST], fetchAccount);
} }
@ -389,6 +394,7 @@ export function *initBlockHeader() {
return; return;
} }
yield put({type: actions.BLOCKS[actions.REQUEST]}); 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]}); yield put({type: actions.TRANSACTIONS[actions.REQUEST]});
} }
} }
@ -568,6 +574,7 @@ export default function *root() {
fork(watchFetchVersions), fork(watchFetchVersions),
fork(watchFetchPlugins), fork(watchFetchPlugins),
fork(watchFetchBlocks), fork(watchFetchBlocks),
fork(watchFetchBlocksFull),
fork(watchFetchContracts), fork(watchFetchContracts),
fork(watchFetchContractProfile), fork(watchFetchContractProfile),
fork(watchPostContractFunction), fork(watchPostContractFunction),

View File

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