Simple GUI for local transactions

This commit is contained in:
Tomasz Drwięga 2016-11-17 10:15:11 +01:00
parent cd686b5d68
commit ff27bbcb4f
14 changed files with 834 additions and 13 deletions

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { inAddress, inData, inHex, inNumber16, inOptions } from '../../format/input'; import { inAddress, inData, inHex, inNumber16, inOptions } from '../../format/input';
import { outAccountInfo, outAddress, outHistogram, outNumber, outPeers } from '../../format/output'; import { outAccountInfo, outAddress, outHistogram, outNumber, outPeers, outTransaction } from '../../format/output';
export default class Parity { export default class Parity {
constructor (transport) { constructor (transport) {
@ -117,16 +117,27 @@ export default class Parity {
.execute('parity_hashContent', url); .execute('parity_hashContent', url);
} }
importGethAccounts (accounts) {
return this._transport
.execute('parity_importGethAccounts', (accounts || []).map(inAddress))
.then((accounts) => (accounts || []).map(outAddress));
}
listGethAccounts () { listGethAccounts () {
return this._transport return this._transport
.execute('parity_listGethAccounts') .execute('parity_listGethAccounts')
.then((accounts) => (accounts || []).map(outAddress)); .then((accounts) => (accounts || []).map(outAddress));
} }
importGethAccounts (accounts) { localTransactions () {
return this._transport return this._transport
.execute('parity_importGethAccounts', (accounts || []).map(inAddress)) .execute('parity_localTransactions')
.then((accounts) => (accounts || []).map(outAddress)); .then(transactions => {
Object.values(transactions)
.filter(tx => tx.transaction)
.map(tx => tx.transaction = outTransaction(tx.transaction));
return transactions;
});
} }
minGasPrice () { minGasPrice () {
@ -192,6 +203,17 @@ export default class Parity {
.execute('parity_nodeName'); .execute('parity_nodeName');
} }
pendingTransactions () {
return this._transport
.execute('parity_pendingTransactions')
.then(data => data.map(outTransaction));
}
pendingTransactionsStats () {
return this._transport
.execute('parity_pendingTransactionsStats');
}
phraseToAddress (phrase) { phraseToAddress (phrase) {
return this._transport return this._transport
.execute('parity_phraseToAddress', phrase) .execute('parity_phraseToAddress', phrase)

17
js/src/dapps/localtx.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="icon" href="/parity-logo-black-no-text.png" type="image/png">
<title>Local transactions Viewer</title>
</head>
<body>
<div id="container"></div>
<script src="vendor.js"></script>
<script src="commons.js"></script>
<script src="/parity-utils/parity.js"></script>
<script src="localtx.js"></script>
</body>
</html>

33
js/src/dapps/localtx.js Normal file
View File

@ -0,0 +1,33 @@
// 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 <http://www.gnu.org/licenses/>.
import ReactDOM from 'react-dom';
import React from 'react';
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();
import Application from './localtx/Application';
import '../../assets/fonts/Roboto/font.css';
import '../../assets/fonts/RobotoMono/font.css';
import './style.css';
import './localtx.html';
ReactDOM.render(
<Application />,
document.querySelector('#container')
);

View File

@ -0,0 +1,197 @@
// 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 <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import React, { Component } from 'react';
import { api } from '../parity';
import { Transaction, LocalTransaction } from '../Transaction';
export default class Application extends Component {
state = {
loading: true,
transactions: [],
localTransactions: {}
}
componentDidMount () {
const poll = () => this.fetchTransactionData().then(poll);
this._timeout = setTimeout(poll, 2000);
}
componentWillUnmount () {
clearTimeout(this._timeout);
}
fetchTransactionData () {
return Promise.all([
api.parity.pendingTransactions(),
api.parity.pendingTransactionsStats(),
api.parity.localTransactions(),
]).then(([pending, stats, local]) => {
// Combine results together
const transactions = pending.map(tx => {
return {
transaction: tx,
stats: stats[tx.hash],
isLocal: !!local[tx.hash]
};
});
// Add transaction data to locals
transactions
.filter(tx => tx.isLocal)
.map(data => {
const tx = data.transaction;
local[tx.hash].transaction = tx;
local[tx.hash].stats = data.stats;
});
// Convert local transactions to array
const localTransactions = Object.keys(local).map(hash => {
const data = local[hash];
data.txHash = hash;
return data;
});
// Sort local transactions by nonce (move future to the end)
localTransactions.sort((a, b) => {
a = a.transaction || {};
b = b.transaction || {};
if (a.from && b.from && a.from !== b.from) {
return a.from < b.from;
}
if (!a.nonce || !b.nonce) {
return !a.nonce ? 1 : -1;
}
return new BigNumber(a.nonce).comparedTo(new BigNumber(b.nonce));
});
this.setState({
loading: false,
transactions,
localTransactions
});
});
}
render () {
const { loading } = this.state;
if (loading) {
return (
<div>Loading...</div>
);
}
return (
<div style={ { padding: '1rem 2rem'}}>
<h1>Your past local transactions</h1>
{ this.renderLocals() }
<h1>Transactions in the queue</h1>
{ this.renderQueueSummary() }
{ this.renderQueue() }
</div>
);
}
renderQueueSummary () {
const { transactions } = this.state;
if (!transactions.length) {
return null;
}
const count = transactions.length;
const locals = transactions.filter(tx => tx.isLocal).length;
const fee = transactions
.map(tx => tx.transaction)
.map(tx => tx.gasPrice.mul(tx.gas))
.reduce((sum, fee) => sum.add(fee), new BigNumber(0));
return (
<h3>
Count: <strong>{ locals ? `${count} (${locals})` : count }</strong>
&nbsp;
Total Fee: <strong>{ api.util.fromWei(fee).toFixed(3) } ETH</strong>
</h3>
);
}
renderQueue () {
const { transactions } = this.state;
if (!transactions.length) {
return (
<h3>The queue seems is empty.</h3>
);
}
return (
<table cellSpacing='0'>
<thead>
{ Transaction.renderHeader() }
</thead>
<tbody>
{
transactions.map((tx, idx) => (
<Transaction
key={ tx.transaction.hash }
idx={ idx + 1}
isLocal={ tx.isLocal }
transaction={ tx.transaction }
stats={ tx.stats }
/>
))
}
</tbody>
</table>
);
}
renderLocals () {
const { localTransactions } = this.state;
if (!localTransactions.length) {
return (
<h3>You haven't sent any transactions yet.</h3>
);
}
return (
<table cellSpacing='0'>
<thead>
{ LocalTransaction.renderHeader() }
</thead>
<tbody>
{
localTransactions.map(tx => (
<LocalTransaction
key={ tx.txHash }
hash={ tx.txHash }
transaction={ tx.transaction }
status={ tx.status }
stats={ tx.stats }
details={ tx }
/>
))
}
</tbody>
</table>
);
}
}

View File

@ -0,0 +1,32 @@
// 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 <http://www.gnu.org/licenses/>.
import React from 'react';
import { shallow } from 'enzyme';
import '../../../environment/tests';
import Application from './application';
describe('localtx/Application', () => {
describe('rendering', () => {
it('renders without crashing', () => {
const rendered = shallow(<Application />);
expect(rendered).to.be.defined;
});
});
});

View File

@ -0,0 +1,17 @@
// 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 <http://www.gnu.org/licenses/>.
export default from './application';

View File

@ -0,0 +1,17 @@
// 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 <http://www.gnu.org/licenses/>.
export { Transaction, LocalTransaction } from './transaction';

View File

@ -0,0 +1,31 @@
.from {
white-space: nowrap;
img {
vertical-align: middle;
}
}
.transaction {
td {
padding: 7px 15px;
}
td:first-child {
padding: 7px 0;
}
&.local {
background: #8bc34a;
}
}
.nowrap {
white-space: nowrap;
}
.edit {
label, input {
display: block;
}
}

View File

@ -0,0 +1,359 @@
// 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 <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import classnames from 'classnames';
import { api } from '../parity';
import styles from './transaction.css';
import IdentityIcon from '../../githubhint/IdentityIcon';
class BaseTransaction extends Component {
shortHash (hash) {
return `${hash.substr(0, 6)}..${hash.substr(hash.length - 4)}`;
}
renderHash (hash) {
return (
<code title={ hash }>
{ this.shortHash(hash) }
</code>
);
}
renderFrom (transaction) {
if (!transaction) {
return '-';
}
return (
<div title={ transaction.from } className={ styles.from }>
<IdentityIcon
address={ transaction.from }
/>
0x{ transaction.nonce.toString(16) }
</div>
);
}
renderGasPrice (transaction) {
if (!transaction) {
return '-';
}
return (
<span title={ `${transaction.gasPrice.toFormat(0) } wei`}>
{ api.util.fromWei(transaction.gasPrice, 'shannon').toFormat(2) }&nbsp;shannon
</span>
);
}
renderGas (transaction) {
if (!transaction) {
return '-';
}
return (
<span title={ `${transaction.gas.toFormat(0)} Gas` }>
{ transaction.gas.div(10**6).toFormat(3) }&nbsp;MGas
</span>
);
}
renderPropagation (stats) {
const noOfPeers = Object.keys(stats.propagatedTo).length;
const noOfPropagations = Object.values(stats.propagatedTo).reduce((sum, val) => sum + val, 0);
return (
<span className={ styles.nowrap }>
{ noOfPropagations } ({ noOfPeers } peers)
</span>
);
}
}
export class Transaction extends BaseTransaction {
static propTypes = {
idx: PropTypes.number.isRequired,
transaction: PropTypes.object.isRequired,
isLocal: PropTypes.bool,
stats: PropTypes.object
};
static defaultProps = {
isLocal: false,
stats: {
firstSeen: 0,
propagatedTo: {}
}
};
static renderHeader () {
return (
<tr className={ styles.header }>
<th></th>
<th>
Transaction
</th>
<th>
From
</th>
<th>
Gas Price
</th>
<th>
Gas
</th>
<th>
First seen
</th>
<th>
# Propagated
</th>
<th>
</th>
</tr>
);
}
render () {
const { isLocal, stats, transaction, idx } = this.props;
const clazz = classnames(styles.transaction, {
[styles.local]: isLocal
});
const noOfPeers = Object.keys(stats.propagatedTo).length;
const noOfPropagations = Object.values(stats.propagatedTo).reduce((sum, val) => sum + val, 0);
return (
<tr className={ clazz }>
<td>
{ idx }.
</td>
<td>
{ this.renderHash(transaction.hash) }
</td>
<td>
{ this.renderFrom(transaction) }
</td>
<td>
{ this.renderGasPrice(transaction) }
</td>
<td>
{ this.renderGas(transaction) }
</td>
<td>
{ stats.firstSeen }
</td>
<td>
{ this.renderPropagation(stats) }
</td>
</tr>
);
}
}
export class LocalTransaction extends BaseTransaction {
static propTypes = {
hash: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
transaction: PropTypes.object,
isLocal: PropTypes.bool,
stats: PropTypes.object,
details: PropTypes.object
};
static defaultProps = {
stats: {
propagatedTo: {}
}
};
static renderHeader () {
return (
<tr className={ styles.header }>
<th></th>
<th>
Transaction
</th>
<th>
From
</th>
<th>
Gas Price / Gas
</th>
<th>
Propagated
</th>
<th>
Status
</th>
</tr>
);
}
state = {
isSending: false,
isResubmitting: false,
gasPrice: null,
gas: null
};
toggleResubmit = () => {
const { transaction } = this.props;
const { isResubmitting, gasPrice } = this.state;
this.setState({
isResubmitting: !isResubmitting,
});
if (gasPrice === null) {
this.setState({
gasPrice: `0x${ transaction.gasPrice.toString(16) }`,
gas: `0x${ transaction.gas.toString(16) }`
});
}
};
sendTransaction = () => {
const { transaction } = this.props;
const { gasPrice, gas } = this.state;
const newTransaction = {
from: transaction.from,
to: transaction.to,
nonce: transaction.nonce,
value: transaction.value,
data: transaction.data,
gasPrice, gas
};
this.setState({
isResubmitting: false,
isSending: true
});
const closeSending = () => this.setState({
isSending: false
});
api.eth.sendTransaction(newTransaction)
.then(closeSending)
.catch(closeSending);
};
render () {
if (this.state.isResubmitting) {
return this.renderResubmit();
}
const { stats, transaction, hash, status } = this.props;
const { isSending } = this.state;
const resubmit = isSending ? (
'sending...'
) : (
<a href='javascript:void' onClick={ this.toggleResubmit }>
resubmit
</a>
);
return (
<tr className={ styles.transaction }>
<td>
{ !transaction ? null : resubmit }
</td>
<td>
{ this.renderHash(hash) }
</td>
<td>
{ this.renderFrom(transaction) }
</td>
<td>
{ this.renderGasPrice(transaction) }
<br />
{ this.renderGas(transaction) }
</td>
<td>
{ status === 'pending' ? this.renderPropagation(stats) : '-' }
</td>
<td>
{ this.renderStatus() }
</td>
</tr>
);
}
renderStatus () {
const { details } = this.props;
let state = {
'pending': () => `In queue: Pending`,
'future': () => `In queue: Future`,
'mined': () => `Mined`,
'dropped': () => `Dropped because of queue limit`,
'invalid': () => `Transaction is invalid`,
'rejected': () => `Rejected: ${ details.error }`,
'replaced': () => `Replaced by ${ this.shortHash(details.hash) }`,
}[this.props.status];
return state ? state() : 'unknown';
}
renderResubmit () {
const { transaction } = this.props;
const { gasPrice, gas } = this.state;
return (
<tr className={ styles.transaction }>
<td>
<a href='javascript:void' onClick={ this.toggleResubmit }>
cancel
</a>
</td>
<td>
{ this.renderHash(transaction.hash) }
</td>
<td>
{ this.renderFrom(transaction) }
</td>
<td className={ styles.edit }>
<input
type='text'
value={ gasPrice }
onChange={ el => this.setState({ gasPrice: el.target.value }) }
/>
<input
type='text'
value={ gas }
onChange={ el => this.setState({ gas: el.target.value }) }
/>
</td>
<td colSpan='2'>
<a href='javascript:void' onClick={ this.sendTransaction }>
Send
</a>
</td>
</tr>
);
}
}

View File

@ -0,0 +1,37 @@
// 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 <http://www.gnu.org/licenses/>.
import React from 'react';
import { shallow } from 'enzyme';
import '../../../environment/tests';
import Transaction from './transaction';
describe('localtx/Transaction', () => {
describe('rendering', () => {
it('renders without crashing', () => {
const rendered = shallow(
<Transaction
isLocal={ false }
transaction={ {} }
/>
);
expect(rendered).to.be.defined;
});
});
});

View File

@ -0,0 +1,21 @@
// 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 <http://www.gnu.org/licenses/>.
const api = window.parent.secureApi;
export {
api
};

View File

@ -224,15 +224,6 @@ export default {
} }
}, },
listGethAccounts: {
desc: 'Returns a list of the accounts available from Geth',
params: [],
returns: {
type: Array,
desc: '20 Bytes addresses owned by the client.'
}
},
importGethAccounts: { importGethAccounts: {
desc: 'Imports a list of accounts from geth', desc: 'Imports a list of accounts from geth',
params: [ params: [
@ -247,6 +238,24 @@ export default {
} }
}, },
listGethAccounts: {
desc: 'Returns a list of the accounts available from Geth',
params: [],
returns: {
type: Array,
desc: '20 Bytes addresses owned by the client.'
}
},
localTransactions: {
desc: 'Returns an object of current and past local transactions.',
params: [],
returns: {
type: Object,
desc: 'Mapping of `tx hash` into status object.'
}
},
minGasPrice: { minGasPrice: {
desc: 'Returns currently set minimal gas price', desc: 'Returns currently set minimal gas price',
params: [], params: [],
@ -379,6 +388,24 @@ export default {
} }
}, },
pendingTransactions: {
desc: 'Returns a list of transactions currently in the queue.',
params: [],
returns: {
type: Array,
desc: 'Transactions ordered by priority'
}
},
pendingTransactionsStats: {
desc: 'Returns propagation stats for transactions in the queue',
params: [],
returns: {
type: Object,
desc: 'mapping of `tx hash` into `stats`'
}
},
phraseToAddress: { phraseToAddress: {
desc: 'Converts a secret phrase into the corresponting address', desc: 'Converts a secret phrase into the corresponting address',
params: [ params: [

View File

@ -39,5 +39,15 @@
"author": "Parity Team <admin@ethcore.io>", "author": "Parity Team <admin@ethcore.io>",
"version": "1.0.0", "version": "1.0.0",
"secure": true "secure": true
},
{
"id": "0xae74ad174b95cdbd01c88ac5b73a296d33e9088fc2a200e76bcedf3a94a7815d",
"url": "localtx",
"name": "TxQueue Viewer",
"description": "Have a peak on internals of transaction queue of your node.",
"author": "Parity Team <admin@ethcore.io>",
"version": "1.0.0",
"secure": true
} }
] ]

View File

@ -40,6 +40,7 @@ module.exports = {
'githubhint': ['./dapps/githubhint.js'], 'githubhint': ['./dapps/githubhint.js'],
'registry': ['./dapps/registry.js'], 'registry': ['./dapps/registry.js'],
'signaturereg': ['./dapps/signaturereg.js'], 'signaturereg': ['./dapps/signaturereg.js'],
'localtx': ['./dapps/localtx.js'],
'tokenreg': ['./dapps/tokenreg.js'], 'tokenreg': ['./dapps/tokenreg.js'],
// app // app
'index': ['./index.js'] 'index': ['./index.js']