From 76cded7fa409bfe9fa353d9513b13f7704234f7d Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 22 Oct 2016 09:45:54 +0200 Subject: [PATCH] Use trace API for decentralized transaction list (#2784) * Using traces when available to get accounts transactions (#2148) * Fixed traceMode detection and transactions rendering (#2148) * [WIP] Use Redux Thunk in main UI => Async Actions (#2148) * Using Redux for Transaction / Block / Methods... (#2148) * Use BigNumber comparedTo function to sort txs (#2148) --- js/src/api/format/input.js | 21 +++ js/src/api/rpc/trace/trace.js | 4 +- js/src/index.js | 3 + js/src/redux/middleware.js | 3 +- js/src/redux/providers/apiActions.js | 22 +++ js/src/redux/providers/apiReducer.js | 26 ++++ js/src/redux/providers/blockchainActions.js | 128 ++++++++++++++++ js/src/redux/providers/blockchainReducer.js | 66 ++++++++ js/src/redux/providers/index.js | 2 + js/src/redux/providers/status.js | 18 ++- js/src/redux/providers/statusReducer.js | 3 +- js/src/redux/reducers.js | 4 +- js/src/ui/MethodDecoding/methodDecoding.js | 119 +++++++++------ js/src/util/validation.js | 1 - .../Transactions/Transaction/transaction.js | 67 +++++---- .../Account/Transactions/transactions.js | 141 ++++++++++++++---- 16 files changed, 519 insertions(+), 109 deletions(-) create mode 100644 js/src/redux/providers/apiActions.js create mode 100644 js/src/redux/providers/apiReducer.js create mode 100644 js/src/redux/providers/blockchainActions.js create mode 100644 js/src/redux/providers/blockchainReducer.js diff --git a/js/src/api/format/input.js b/js/src/api/format/input.js index 27c834ead..b46148cdc 100644 --- a/js/src/api/format/input.js +++ b/js/src/api/format/input.js @@ -141,3 +141,24 @@ export function inOptions (options) { return options; } + +export function inTraceFilter (filterObject) { + if (filterObject) { + Object.keys(filterObject).forEach((key) => { + switch (key) { + case 'fromAddress': + case 'toAddress': + filterObject[key] = [].concat(filterObject[key]) + .map(address => inAddress(address)); + break; + + case 'toBlock': + case 'fromBlock': + filterObject[key] = inBlockNumber(filterObject[key]); + break; + } + }); + } + + return filterObject; +} diff --git a/js/src/api/rpc/trace/trace.js b/js/src/api/rpc/trace/trace.js index 93a35c7f3..95fed4230 100644 --- a/js/src/api/rpc/trace/trace.js +++ b/js/src/api/rpc/trace/trace.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { inBlockNumber, inHex, inNumber16 } from '../../format/input'; +import { inBlockNumber, inHex, inNumber16, inTraceFilter } from '../../format/input'; import { outTrace } from '../../format/output'; export default class Trace { @@ -24,7 +24,7 @@ export default class Trace { filter (filterObj) { return this._transport - .execute('trace_filter', filterObj) + .execute('trace_filter', inTraceFilter(filterObj)) .then(traces => traces.map(trace => outTrace(trace))); } diff --git a/js/src/index.js b/js/src/index.js index db173a07f..08b02c2a3 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -39,6 +39,8 @@ import { initStore } from './redux'; import { ContextProvider, muiTheme } from './ui'; import { Accounts, Account, Addresses, Address, Application, Contract, Contracts, Dapp, Dapps, Settings, SettingsBackground, SettingsProxy, SettingsViews, Signer, Status } from './views'; +import { setApi } from './redux/providers/apiActions'; + import './environment'; import '../assets/fonts/Roboto/font.css'; @@ -60,6 +62,7 @@ ContractInstances.create(api); const store = initStore(api); store.dispatch({ type: 'initAll', api }); +store.dispatch(setApi(api)); const routerHistory = useRouterHistory(createHashHistory)({}); diff --git a/js/src/redux/middleware.js b/js/src/redux/middleware.js index 6ba811ea7..a54f2fae4 100644 --- a/js/src/redux/middleware.js +++ b/js/src/redux/middleware.js @@ -13,6 +13,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import thunk from 'redux-thunk'; import ErrorsMiddleware from '../ui/Errors/middleware'; import SettingsMiddleware from '../views/Settings/middleware'; @@ -32,5 +33,5 @@ export default function (api) { errors.toMiddleware() ]; - return middleware.concat(status); + return middleware.concat(status, thunk); } diff --git a/js/src/redux/providers/apiActions.js b/js/src/redux/providers/apiActions.js new file mode 100644 index 000000000..88e1fa848 --- /dev/null +++ b/js/src/redux/providers/apiActions.js @@ -0,0 +1,22 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export function setApi (api) { + return { + type: 'setApi', + api + }; +} diff --git a/js/src/redux/providers/apiReducer.js b/js/src/redux/providers/apiReducer.js new file mode 100644 index 000000000..2d4c96ecd --- /dev/null +++ b/js/src/redux/providers/apiReducer.js @@ -0,0 +1,26 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { handleActions } from 'redux-actions'; + +const initialState = {}; + +export default handleActions({ + setApi (state, action) { + const { api } = action; + return api; + } +}, initialState); diff --git a/js/src/redux/providers/blockchainActions.js b/js/src/redux/providers/blockchainActions.js new file mode 100644 index 000000000..eac2bf62b --- /dev/null +++ b/js/src/redux/providers/blockchainActions.js @@ -0,0 +1,128 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import Contracts from '../../contracts'; + +export function setBlock (blockNumber, block) { + return { + type: 'setBlock', + blockNumber, block + }; +} + +export function setTransaction (txHash, info) { + return { + type: 'setTransaction', + txHash, info + }; +} + +export function setBytecode (address, bytecode) { + return { + type: 'setBytecode', + address, bytecode + }; +} + +export function setMethod (signature, method) { + return { + type: 'setMethod', + signature, method + }; +} + +export function fetchBlock (blockNumber) { + return (dispatch, getState) => { + const { blocks } = getState().blockchain; + + if (blocks[blockNumber.toString()]) { + return; + } + + const { api } = getState(); + + api.eth + .getBlockByNumber(blockNumber) + .then(block => { + dispatch(setBlock(blockNumber, block)); + }) + .catch(e => { + console.error('blockchain::fetchBlock', e); + }); + }; +} + +export function fetchTransaction (txHash) { + return (dispatch, getState) => { + const { transactions } = getState().blockchain; + + if (transactions[txHash]) { + return; + } + + const { api } = getState(); + + api.eth + .getTransactionByHash(txHash) + .then(info => { + dispatch(setTransaction(txHash, info)); + }) + .catch(e => { + console.error('blockchain::fetchTransaction', e); + }); + }; +} + +export function fetchBytecode (address) { + return (dispatch, getState) => { + const { bytecodes } = getState().blockchain; + + if (bytecodes[address]) { + return; + } + + const { api } = getState(); + + api.eth + .getCode(address) + .then(code => { + dispatch(setBytecode(address, code)); + }) + .catch(e => { + console.error('blockchain::fetchBytecode', e); + }); + }; +} + +export function fetchMethod (signature) { + return (dispatch, getState) => { + const { methods } = getState().blockchain; + + if (methods[signature]) { + return; + } + + Contracts + .get() + .signatureReg.lookup(signature) + .then(method => { + dispatch(setMethod(signature, method)); + }) + .catch(e => { + console.error('blockchain::fetchMethod', e); + }); + }; +} diff --git a/js/src/redux/providers/blockchainReducer.js b/js/src/redux/providers/blockchainReducer.js new file mode 100644 index 000000000..30c7507f1 --- /dev/null +++ b/js/src/redux/providers/blockchainReducer.js @@ -0,0 +1,66 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { handleActions } from 'redux-actions'; + +const initialState = { + blocks: {}, + transactions: {}, + bytecodes: {}, + methods: {} +}; + +export default handleActions({ + setBlock (state, action) { + const { blockNumber, block } = action; + + const blocks = Object.assign({}, state.blocks, { + [blockNumber.toString()]: block + }); + + return Object.assign({}, state, { blocks }); + }, + + setTransaction (state, action) { + const { txHash, info } = action; + + const transactions = Object.assign({}, state.transactions, { + [txHash]: info + }); + + return Object.assign({}, state, { transactions }); + }, + + setBytecode (state, action) { + const { address, bytecode } = action; + + const bytecodes = Object.assign({}, state.bytecodes, { + [address]: bytecode + }); + + return Object.assign({}, state, { bytecodes }); + }, + + setMethod (state, action) { + const { signature, method } = action; + + const methods = Object.assign({}, state.methods, { + [signature]: method + }); + + return Object.assign({}, state, { methods }); + } +}, initialState); diff --git a/js/src/redux/providers/index.js b/js/src/redux/providers/index.js index 7e5f19f2b..a83190a10 100644 --- a/js/src/redux/providers/index.js +++ b/js/src/redux/providers/index.js @@ -19,8 +19,10 @@ export Personal from './personal'; export Signer from './signer'; export Status from './status'; +export apiReducer from './apiReducer'; export balancesReducer from './balancesReducer'; export imagesReducer from './imagesReducer'; export personalReducer from './personalReducer'; export signerReducer from './signerReducer'; export statusReducer from './statusReducer'; +export blockchainReducer from './blockchainReducer'; diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 917a8577e..2e039134b 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -54,6 +54,16 @@ export default class Status { .catch(() => dispatch(false)); } + _pollTraceMode = () => { + return this._api.trace.block() + .then(blockTraces => { + // Assumes not in Trace Mode if no transactions + // in latest block... + return blockTraces.length > 0; + }) + .catch(() => false); + } + _pollStatus = () => { const { secureToken, isConnected, isConnecting, needsToken } = this._api; const nextTimeout = (timeout = 1000) => { @@ -80,9 +90,10 @@ export default class Status { this._api.ethcore.netPort(), this._api.ethcore.nodeName(), this._api.ethcore.rpcSettings(), - this._api.eth.syncing() + this._api.eth.syncing(), + this._pollTraceMode() ]) - .then(([clientVersion, coinbase, defaultExtraData, extraData, gasFloorTarget, hashrate, minGasPrice, netChain, netPeers, netPort, nodeName, rpcSettings, syncing]) => { + .then(([clientVersion, coinbase, defaultExtraData, extraData, gasFloorTarget, hashrate, minGasPrice, netChain, netPeers, netPort, nodeName, rpcSettings, syncing, traceMode]) => { const isTest = netChain === 'morden' || netChain === 'testnet'; this._store.dispatch(statusCollection({ @@ -99,7 +110,8 @@ export default class Status { nodeName, rpcSettings, syncing, - isTest + isTest, + traceMode })); nextTimeout(); }) diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js index 18a60284b..d75f62429 100644 --- a/js/src/redux/providers/statusReducer.js +++ b/js/src/redux/providers/statusReducer.js @@ -41,7 +41,8 @@ const initialState = { syncing: false, isApiConnected: true, isPingConnected: true, - isTest: true + isTest: false, + traceMode: undefined }; export default handleActions({ diff --git a/js/src/redux/reducers.js b/js/src/redux/reducers.js index 4161807ba..ee73b5cdf 100644 --- a/js/src/redux/reducers.js +++ b/js/src/redux/reducers.js @@ -17,7 +17,7 @@ import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux'; -import { balancesReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer } from './providers'; +import { apiReducer, balancesReducer, blockchainReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer } from './providers'; import { errorReducer } from '../ui/Errors'; import { settingsReducer } from '../views/Settings'; @@ -25,12 +25,14 @@ import { tooltipReducer } from '../ui/Tooltips'; export default function () { return combineReducers({ + api: apiReducer, errors: errorReducer, tooltip: tooltipReducer, routing: routerReducer, settings: settingsReducer, balances: balancesReducer, + blockchain: blockchainReducer, images: imagesReducer, nodeStatus: nodeStatusReducer, personal: personalReducer, diff --git a/js/src/ui/MethodDecoding/methodDecoding.js b/js/src/ui/MethodDecoding/methodDecoding.js index 741716428..21e7730e8 100644 --- a/js/src/ui/MethodDecoding/methodDecoding.js +++ b/js/src/ui/MethodDecoding/methodDecoding.js @@ -19,11 +19,12 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import Contracts from '../../contracts'; import IdentityIcon from '../IdentityIcon'; import IdentityName from '../IdentityName'; import { Input, InputAddress } from '../Form'; +import { fetchBytecode, fetchMethod } from '../../redux/providers/blockchainActions'; + import styles from './methodDecoding.css'; const CONTRACT_CREATE = '0x60606040'; @@ -31,7 +32,7 @@ const TOKEN_METHODS = { '0xa9059cbb': 'transfer(to,value)' }; -class Method extends Component { +class MethodDecoding extends Component { static contextTypes = { api: PropTypes.object.isRequired } @@ -40,10 +41,16 @@ class Method extends Component { address: PropTypes.string.isRequired, tokens: PropTypes.object, transaction: PropTypes.object, - historic: PropTypes.bool + historic: PropTypes.bool, + + fetchBytecode: PropTypes.func, + fetchMethod: PropTypes.func, + bytecodes: PropTypes.object, + methods: PropTypes.object } state = { + contractAddress: null, method: null, methodName: null, methodInputs: null, @@ -55,20 +62,59 @@ class Method extends Component { isReceived: false } - componentDidMount () { + componentWillMount () { const { transaction } = this.props; - this.lookup(transaction); } + componentDidMount () { + this.setMethod(this.props); + } + componentWillReceiveProps (newProps) { const { transaction } = this.props; + this.setMethod(newProps); - if (newProps.transaction === transaction) { + if (newProps.transaction.hash !== transaction.hash) { + this.lookup(transaction); return; } + } - this.lookup(transaction); + setMethod (props) { + const { bytecodes, methods } = props; + const { contractAddress, methodSignature, methodParams } = this.state; + + if (contractAddress && bytecodes[contractAddress]) { + const bytecode = bytecodes[contractAddress]; + + if (bytecode && bytecode !== '0x') { + this.setState({ isContract: true }); + } + } + + if (methodSignature && methods[methodSignature]) { + const method = methods[methodSignature]; + const { api } = this.context; + + let methodInputs = null; + let methodName = null; + + if (method && method.length) { + const abi = api.util.methodToAbi(method); + + methodName = abi.name; + methodInputs = api.util + .decodeMethodInput(abi, methodParams) + .map((value, index) => { + const type = abi.inputs[index].type; + + return { type, value }; + }); + } + + this.setState({ method, methodName, methodInputs }); + } } render () { @@ -150,7 +196,7 @@ class Method extends Component { return (
- Deployed a contract at address { this.renderAddressName(transaction.creates, false) }. + Deployed a contract at address { this.renderAddressName(transaction.creates, false) }
); } @@ -275,18 +321,20 @@ class Method extends Component { } lookup (transaction) { - const { api } = this.context; - const { address, tokens } = this.props; - if (!transaction) { return; } - const isReceived = transaction.to === address; - const token = (tokens || {})[isReceived ? transaction.from : transaction.to]; - this.setState({ token, isReceived }); + const { api } = this.context; + const { address, tokens } = this.props; - if (!transaction.input) { + const isReceived = transaction.to === address; + const contractAddress = isReceived ? transaction.from : transaction.to; + + const token = (tokens || {})[contractAddress]; + this.setState({ token, isReceived, contractAddress }); + + if (!transaction.input || transaction.input === '0x') { return; } @@ -298,52 +346,29 @@ class Method extends Component { return; } - api.eth - .getCode(isReceived ? transaction.from : transaction.to) - .then((code) => { - if (code && code !== '0x') { - this.setState({ isContract: true }); - } + const { fetchBytecode, fetchMethod } = this.props; - return Contracts.get().signatureReg.lookup(signature); - }).then((method) => { - let methodInputs = null; - let methodName = null; - - if (method && method.length) { - const abi = api.util.methodToAbi(method); - - methodName = abi.name; - methodInputs = api.util - .decodeMethodInput(abi, paramdata) - .map((value, index) => { - const type = abi.inputs[index].type; - - return { type, value }; - }); - } - - this.setState({ method, methodName, methodInputs }); - }) - .catch((error) => { - console.error('lookup', error); - }); + fetchBytecode(contractAddress); + fetchMethod(signature); } } function mapStateToProps (state) { const { tokens } = state.balances; + const { bytecodes, methods } = state.blockchain; return { - tokens + tokens, bytecodes, methods }; } function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); + return bindActionCreators({ + fetchBytecode, fetchMethod + }, dispatch); } export default connect( mapStateToProps, mapDispatchToProps -)(Method); +)(MethodDecoding); diff --git a/js/src/util/validation.js b/js/src/util/validation.js index d6775d237..1ec7489f8 100644 --- a/js/src/util/validation.js +++ b/js/src/util/validation.js @@ -38,7 +38,6 @@ export function validateAbi (abi, api) { abi = JSON.stringify(abiParsed); } } catch (error) { - console.error(error); abiError = ERRORS.invalidAbi; } diff --git a/js/src/views/Account/Transactions/Transaction/transaction.js b/js/src/views/Account/Transactions/Transaction/transaction.js index 4d8060eca..7cf2bce31 100644 --- a/js/src/views/Account/Transactions/Transaction/transaction.js +++ b/js/src/views/Account/Transactions/Transaction/transaction.js @@ -17,12 +17,16 @@ import BigNumber from 'bignumber.js'; import React, { Component, PropTypes } from 'react'; import moment from 'moment'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import { fetchBlock, fetchTransaction } from '../../../../redux/providers/blockchainActions'; import { IdentityIcon, IdentityName, MethodDecoding } from '../../../../ui'; import styles from '../transactions.css'; -export default class Transaction extends Component { +class Transaction extends Component { static contextTypes = { api: PropTypes.object.isRequired } @@ -30,11 +34,16 @@ export default class Transaction extends Component { static propTypes = { transaction: PropTypes.object.isRequired, address: PropTypes.string.isRequired, - isTest: PropTypes.bool.isRequired + isTest: PropTypes.bool.isRequired, + + fetchBlock: PropTypes.func.isRequired, + fetchTransaction: PropTypes.func.isRequired, + + block: PropTypes.object, + transactionInfo: PropTypes.object } state = { - info: null, isContract: false, isReceived: false } @@ -46,8 +55,7 @@ export default class Transaction extends Component { } render () { - const { transaction, isTest } = this.props; - const { block } = this.state; + const { block, transaction, isTest } = this.props; const prefix = `https://${isTest ? 'testnet.' : ''}etherscan.io/`; @@ -68,10 +76,9 @@ export default class Transaction extends Component { } renderMethod () { - const { address } = this.props; - const { info } = this.state; + const { address, transactionInfo } = this.props; - if (!info) { + if (!transactionInfo) { return null; } @@ -79,7 +86,7 @@ export default class Transaction extends Component { + transaction={ transactionInfo } /> ); } @@ -129,13 +136,13 @@ export default class Transaction extends Component { renderEtherValue () { const { api } = this.context; - const { info } = this.state; + const { transactionInfo } = this.props; - if (!info) { + if (!transactionInfo) { return null; } - const value = api.util.fromWei(info.value); + const value = api.util.fromWei(transactionInfo.value); if (value.eq(0)) { return
{ ' ' }
; @@ -169,25 +176,33 @@ export default class Transaction extends Component { } lookup (address, transaction) { - const { api } = this.context; - const { info } = this.state; + const { transactionInfo } = this.props; - if (info) { + if (transactionInfo) { return; } this.setState({ isReceived: address === transaction.to }); - Promise - .all([ - api.eth.getBlockByNumber(transaction.blockNumber), - api.eth.getTransactionByHash(transaction.hash) - ]) - .then(([block, info]) => { - this.setState({ block, info }); - }) - .catch((error) => { - console.error('lookup', error); - }); + const { fetchBlock, fetchTransaction } = this.props; + const { blockNumber, hash } = transaction; + + fetchBlock(blockNumber); + fetchTransaction(hash); } } + +function mapStateToProps () { + return {}; +} + +function mapDispatchToProps (dispatch) { + return bindActionCreators({ + fetchBlock, fetchTransaction + }, dispatch); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Transaction); diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js index 1c9187660..77bdf60a3 100644 --- a/js/src/views/Account/Transactions/transactions.js +++ b/js/src/views/Account/Transactions/transactions.js @@ -37,7 +37,10 @@ class Transactions extends Component { contacts: PropTypes.object, contracts: PropTypes.object, tokens: PropTypes.object, - isTest: PropTypes.bool + isTest: PropTypes.bool, + traceMode: PropTypes.bool, + blocks: PropTypes.object, + transactionsInfo: PropTypes.object } state = { @@ -47,7 +50,24 @@ class Transactions extends Component { } componentDidMount () { - this.getTransactions(); + if (this.props.traceMode !== undefined) { + this.getTransactions(this.props); + } + } + + componentWillReceiveProps (newProps) { + if (this.props.traceMode === undefined && newProps.traceMode !== undefined) { + this.getTransactions(newProps); + return; + } + + const hasChanged = [ 'isTest', 'address' ] + .map(key => newProps[key] !== this.props[key]) + .reduce((truth, keyTruth) => truth || keyTruth, false); + + if (hasChanged) { + this.getTransactions(newProps); + } } render () { @@ -81,60 +101,127 @@ class Transactions extends Component { { this.renderRows() } -
- Transaction list powered by etherscan.io -
+ { this.renderEtherscanFooter() } + + ); + } + + renderEtherscanFooter () { + const { traceMode } = this.props; + + if (traceMode) { + return null; + } + + return ( +
+ Transaction list powered by etherscan.io
); } renderRows () { - const { address, accounts, contacts, contracts, tokens, isTest } = this.props; + const { address, accounts, contacts, contracts, tokens, isTest, blocks, transactionsInfo } = this.props; const { transactions } = this.state; - return (transactions || []).map((transaction, index) => { - return ( - - ); - }); + return (transactions || []) + .sort((tA, tB) => { + return tB.blockNumber.comparedTo(tA.blockNumber); + }) + .slice(0, 25) + .map((transaction, index) => { + const { blockNumber, hash } = transaction; + + const block = blocks[blockNumber.toString()]; + const transactionInfo = transactionsInfo[hash]; + + return ( + + ); + }); } - getTransactions = () => { - const { isTest, address } = this.props; + getTransactions = (props) => { + const { isTest, address, traceMode } = props; - return etherscan.account - .transactions(address, 0, isTest) - .then((transactions) => { + return this + .fetchTransactions(isTest, address, traceMode) + .then(transactions => { this.setState({ transactions, loading: false }); - }) + }); + } + + fetchTransactions = (isTest, address, traceMode) => { + if (traceMode) { + return this.fetchTraceTransactions(address); + } + + return this.fetchEtherscanTransactions(isTest, address); + } + + fetchEtherscanTransactions = (isTest, address) => { + return etherscan.account + .transactions(address, 0, isTest) .catch((error) => { console.error('getTransactions', error); }); } + + fetchTraceTransactions = (address) => { + return Promise + .all([ + this.context.api.trace + .filter({ + fromBlock: 0, + fromAddress: address + }), + this.context.api.trace + .filter({ + fromBlock: 0, + toAddress: address + }) + ]) + .then(([fromTransactions, toTransactions]) => { + const transactions = [].concat(fromTransactions, toTransactions); + + return transactions.map(transaction => ({ + from: transaction.action.from, + to: transaction.action.to, + blockNumber: transaction.blockNumber, + hash: transaction.transactionHash + })); + }); + } } function mapStateToProps (state) { - const { isTest } = state.nodeStatus; + const { isTest, traceMode } = state.nodeStatus; const { accounts, contacts, contracts } = state.personal; const { tokens } = state.balances; + const { blocks, transactions } = state.blockchain; return { isTest, + traceMode, accounts, contacts, contracts, - tokens + tokens, + blocks, + transactionsInfo: transactions }; }