Adding transactions explorer

This commit is contained in:
Anthony Laibe 2018-08-02 14:40:13 +01:00 committed by Pascal Precht
parent ede5afa6e0
commit 20bf924687
No known key found for this signature in database
GPG Key ID: 0EE28D8D6FD85D7D
11 changed files with 235 additions and 35 deletions

View File

@ -10,6 +10,10 @@ export const RECEIVE_PROCESSES_ERROR = 'RECEIVE_PROCESSES_ERROR';
export const FETCH_BLOCKS = 'FETCH_BLOCKS'; export const FETCH_BLOCKS = 'FETCH_BLOCKS';
export const RECEIVE_BLOCKS = 'RECEIVE_BLOCKS'; export const RECEIVE_BLOCKS = 'RECEIVE_BLOCKS';
export const RECEIVE_BLOCKS_ERROR = 'RECEIVE_BLOCKS_ERROR'; export const RECEIVE_BLOCKS_ERROR = 'RECEIVE_BLOCKS_ERROR';
// Transactions
export const FETCH_TRANSACTIONS = 'FETCH_TRANSACTIONS';
export const RECEIVE_TRANSACTIONS = 'RECEIVE_TRANSACTIONS';
export const RECEIVE_TRANSACTIONS_ERROR = 'RECEIVE_TRANSACTIONS_ERROR';
export function fetchAccounts() { export function fetchAccounts() {
return { return {
@ -68,3 +72,23 @@ export function receiveBlocksError() {
type: RECEIVE_BLOCKS_ERROR type: RECEIVE_BLOCKS_ERROR
}; };
} }
export function fetchTransactions(blockFrom) {
return {
type: FETCH_TRANSACTIONS,
blockFrom
};
}
export function receiveTransactions(transactions) {
return {
type: RECEIVE_TRANSACTIONS,
transactions
};
}
export function receiveTransactionsError() {
return {
type: RECEIVE_TRANSACTIONS_ERROR
};
}

View File

@ -10,6 +10,10 @@ export function fetchBlocks(from) {
return axios.get(`${BASE_URL}/blockchain/blocks`, {params: {from}}); return axios.get(`${BASE_URL}/blockchain/blocks`, {params: {from}});
} }
export function fetchTransactions(blockFrom) {
return axios.get(`${BASE_URL}/blockchain/transactions`, {params: {blockFrom}});
}
export function fetchProcesses() { export function fetchProcesses() {
return axios.get(`${BASE_URL}/processes`); return axios.get(`${BASE_URL}/processes`);
} }

View File

@ -8,6 +8,7 @@ import {
import AccountsContainer from '../containers/AccountsContainer'; import AccountsContainer from '../containers/AccountsContainer';
import BlocksContainer from '../containers/BlocksContainer'; import BlocksContainer from '../containers/BlocksContainer';
import TransactionsContainer from '../containers/TransactionsContainer';
const ExplorerLayout = () => ( const ExplorerLayout = () => (
<Grid.Row> <Grid.Row>
@ -31,6 +32,14 @@ const ExplorerLayout = () => (
> >
Blocks Blocks
</List.GroupItem> </List.GroupItem>
<List.GroupItem
className="d-flex align-items-center"
to="/embark/explorer/transactions"
icon="activity"
RootComponent={withRouter(NavLink)}
>
Transactions
</List.GroupItem>
</List.Group> </List.Group>
</div> </div>
</Grid.Col> </Grid.Col>
@ -38,6 +47,7 @@ const ExplorerLayout = () => (
<Switch> <Switch>
<Route exact path="/embark/explorer/accounts" component={AccountsContainer} /> <Route exact path="/embark/explorer/accounts" component={AccountsContainer} />
<Route exact path="/embark/explorer/blocks" component={BlocksContainer} /> <Route exact path="/embark/explorer/blocks" component={BlocksContainer} />
<Route exact path="/embark/explorer/transactions" component={TransactionsContainer} />
</Switch> </Switch>
</Grid.Col> </Grid.Col>
</Grid.Row> </Grid.Row>

View File

@ -0,0 +1,46 @@
import React from 'react';
import {
Page,
Grid,
Card,
Table
} from "tabler-react";
import PropTypes from 'prop-types';
const Transactions = ({transactions}) => (
<Page.Content title="Transactions">
<Grid.Row>
<Grid.Col>
<Card>
<Table
responsive
className="card-table table-vcenter text-nowrap"
headerItems={[
{content: "Block Number"},
{content: "From"},
{content: "To"},
{content: "Type"},
{content: "Hash"}]}
bodyItems={
transactions.map((transaction) => {
return ([
{content: transaction.blockNumber},
{content: transaction.from},
{content: transaction.to},
{content: transaction.to ? "Contract Call" : "Contract Creation"},
{content: transaction.hash}
]);
})
}
/>
</Card>
</Grid.Col>
</Grid.Row>
</Page.Content>
);
Transactions.propTypes = {
transactions: PropTypes.arrayOf(PropTypes.object)
};
export default Transactions;

View File

@ -8,7 +8,6 @@ import Loading from '../components/Loading';
import LoadMore from '../components/LoadMore'; import LoadMore from '../components/LoadMore';
class BlocksContainer extends Component { class BlocksContainer extends Component {
componentDidMount() { componentDidMount() {
if (!this.props.blocks.data) { if (!this.props.blocks.data) {
this.props.fetchBlocks(); this.props.fetchBlocks();

View File

@ -0,0 +1,63 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {fetchTransactions} from '../actions';
import Transactions from '../components/Transactions';
import Loading from '../components/Loading';
import LoadMore from '../components/LoadMore';
class TransactionsContainer extends Component {
componentDidMount() {
if (!this.props.transactions.data) {
this.props.fetchTransactions();
}
}
loadMore() {
this.props.fetchTransactions(this.loadMoreFrom());
}
loadMoreFrom() {
let transactions = this.props.transactions.data;
return transactions[transactions.length - 1].blockNumber - 1;
}
render() {
const {transactions} = this.props;
if (!transactions.data) {
return <Loading />;
}
if (transactions.error) {
return (
<h1>
<i>Error API...</i>
</h1>
);
}
return (
<React.Fragment>
<Transactions transactions={transactions.data}/>
{(this.loadMoreFrom() > 0) ? <LoadMore loadMore={() => this.loadMore()} /> : <React.Fragment />}
</React.Fragment>
);
}
}
function mapStateToProps(state) {
return {transactions: state.transactions};
}
TransactionsContainer.propTypes = {
transactions: PropTypes.object,
fetchTransactions: PropTypes.func
};
export default connect(
mapStateToProps,
{
fetchTransactions
},
)(TransactionsContainer);

View File

@ -3,7 +3,7 @@ import {RECEIVE_ACCOUNTS, RECEIVE_ACCOUNTS_ERROR} from "../actions";
export default function accounts(state = {}, action) { export default function accounts(state = {}, action) {
switch (action.type) { switch (action.type) {
case RECEIVE_ACCOUNTS: case RECEIVE_ACCOUNTS:
return Object.assign({}, state, {data: action.accounts.data}); return {...state, data: [...state.data || [], ...action.accounts.data]};
case RECEIVE_ACCOUNTS_ERROR: case RECEIVE_ACCOUNTS_ERROR:
return Object.assign({}, state, {error: true}); return Object.assign({}, state, {error: true});
default: default:

View File

@ -2,11 +2,13 @@ import {combineReducers} from 'redux';
import processesReducer from './processesReducer'; import processesReducer from './processesReducer';
import accountsReducer from './accountsReducer'; import accountsReducer from './accountsReducer';
import blocksReducer from './blocksReducer'; import blocksReducer from './blocksReducer';
import transactionsReducer from './transactionsReducer';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
accounts: accountsReducer, accounts: accountsReducer,
processes: processesReducer, processes: processesReducer,
blocks: blocksReducer blocks: blocksReducer,
transactions: transactionsReducer
}); });
export default rootReducer; export default rootReducer;

View File

@ -0,0 +1,12 @@
import {RECEIVE_TRANSACTIONS, RECEIVE_TRANSACTIONS_ERROR} from "../actions";
export default function transactions(state = {}, action) {
switch (action.type) {
case RECEIVE_TRANSACTIONS:
return {...state, data: [...state.data || [], ...action.transactions.data]};
case RECEIVE_TRANSACTIONS_ERROR:
return Object.assign({}, state, {error: true});
default:
return state;
}
}

View File

@ -2,6 +2,19 @@ import * as actions from '../actions';
import * as api from '../api'; import * as api from '../api';
import {all, call, fork, put, takeEvery} from 'redux-saga/effects'; import {all, call, fork, put, takeEvery} from 'redux-saga/effects';
export function *fetchTransactions(payload) {
try {
const transactions = yield call(api.fetchTransactions, payload.blockFrom);
yield put(actions.receiveTransactions(transactions));
} catch (e) {
yield put(actions.receiveTransactionsError());
}
}
export function *watchFetchTransactions() {
yield takeEvery(actions.FETCH_TRANSACTIONS, fetchTransactions);
}
export function *fetchBlocks(payload) { export function *fetchBlocks(payload) {
try { try {
const blocks = yield call(api.fetchBlocks, payload.from); const blocks = yield call(api.fetchBlocks, payload.from);
@ -42,5 +55,10 @@ export function *watchFetchProcesses() {
} }
export default function *root() { export default function *root() {
yield all([fork(watchFetchAccounts), fork(watchFetchProcesses), fork(watchFetchBlocks)]); yield all([
fork(watchFetchAccounts),
fork(watchFetchProcesses),
fork(watchFetchBlocks),
fork(watchFetchTransactions)
]);
} }

View File

@ -336,39 +336,61 @@ class BlockchainConnector {
(req, res) => { (req, res) => {
let from = parseInt(req.query.from, 10); let from = parseInt(req.query.from, 10);
let limit = req.query.limit || 10; let limit = req.query.limit || 10;
let results = []; self.getBlocks(from, limit, false, res.send.bind(res));
async.waterfall([
function(callback) {
if (!isNaN(from)) {
return callback();
}
self.getBlockNumber((err, blockNumber) => {
if (err) {
self.logger.error(err);
from = 0;
} else {
from = blockNumber;
}
callback();
});
},
function(callback) {
async.times(limit, function(n, next) {
self.web3.eth.getBlock(from - n, function(err, block) {
if (err){
self.logger.error(err);
return next();
}
results.push(block);
next();
});
}, callback);
}
], function () {
res.send(results);
});
} }
); );
plugin.registerAPICall(
'get',
'/embark-api/blockchain/transactions',
(req, res) => {
let blockFrom = parseInt(req.query.blockFrom, 10);
let blockLimit = req.query.blockLimit || 10;
self.getTransactions(blockFrom, blockLimit, res.send.bind(res));
}
);
}
getTransactions(blockFrom, blockLimit, callback) {
this.getBlocks(blockFrom, blockLimit, true, (blocks) => {
let transactions = blocks.reduce((acc, block) => acc.concat(block.transactions), []);
callback(transactions);
});
}
getBlocks(from, limit, returnTransactionObjects, callback) {
let self = this;
let blocks = [];
async.waterfall([
function(next) {
if (!isNaN(from)) {
return next();
}
self.getBlockNumber((err, blockNumber) => {
if (err) {
self.logger.error(err);
from = 0;
} else {
from = blockNumber;
}
next();
});
},
function(next) {
async.times(limit, function(n, eachCb) {
self.web3.eth.getBlock(from - n, returnTransactionObjects, function(err, block) {
if (err){
self.logger.error(err);
return eachCb();
}
blocks.push(block);
eachCb();
});
}, next);
}
], function () {
callback(blocks);
});
} }