diff --git a/.gitignore b/.gitignore
index f311450394a..c36ae1c9040 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,9 @@
# mac stuff
.DS_Store
+# npm stuff
+npm-debug.log
+
# gdb files
.gdb_history
diff --git a/js/package.json b/js/package.json
index d6c83f07513..f2e1f1770da 100644
--- a/js/package.json
+++ b/js/package.json
@@ -164,6 +164,7 @@
"blockies": "0.0.2",
"brace": "0.9.0",
"bytes": "2.4.0",
+ "date-difference": "1.0.0",
"debounce": "1.0.0",
"es6-error": "4.0.0",
"es6-promise": "4.0.5",
diff --git a/js/src/jsonrpc/interfaces/parity.js b/js/src/jsonrpc/interfaces/parity.js
index e7a5f46c9c7..5d71c54df88 100644
--- a/js/src/jsonrpc/interfaces/parity.js
+++ b/js/src/jsonrpc/interfaces/parity.js
@@ -1833,7 +1833,14 @@ export default {
example: {
from: '0xb60e8dd61c5d32be8058bb8eb970870f07233155',
to: '0xd46e8dd67c5d32be8058bb8eb970870f07244567',
- value: fromDecimal(2441406250)
+ gas: fromDecimal(30400),
+ gasPrice: fromDecimal(10000000000000),
+ value: fromDecimal(2441406250),
+ data: '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675',
+ condition: {
+ block: 354221,
+ time: new Date()
+ }
}
}
],
diff --git a/js/src/ui/MethodDecoding/methodDecoding.js b/js/src/ui/MethodDecoding/methodDecoding.js
index 1d2810c5454..d143cd267e4 100644
--- a/js/src/ui/MethodDecoding/methodDecoding.js
+++ b/js/src/ui/MethodDecoding/methodDecoding.js
@@ -185,14 +185,16 @@ class MethodDecoding extends Component {
);
return (
-
+
+
+
);
}
@@ -204,14 +206,16 @@ class MethodDecoding extends Component {
);
return (
-
+
+
+
);
}
diff --git a/js/src/ui/TxList/TxRow/txRow.js b/js/src/ui/TxList/TxRow/txRow.js
index 1fcf54396cb..c839a04b151 100644
--- a/js/src/ui/TxList/TxRow/txRow.js
+++ b/js/src/ui/TxList/TxRow/txRow.js
@@ -15,6 +15,8 @@
// along with Parity. If not, see .
import moment from 'moment';
+import dateDifference from 'date-difference';
+import { FormattedMessage } from 'react-intl';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
@@ -36,12 +38,15 @@ class TxRow extends Component {
static propTypes = {
accountAddresses: PropTypes.array.isRequired,
address: PropTypes.string.isRequired,
+ blockNumber: PropTypes.object,
contractAddresses: PropTypes.array.isRequired,
netVersion: PropTypes.string.isRequired,
tx: PropTypes.object.isRequired,
block: PropTypes.object,
className: PropTypes.string,
+ cancelTransaction: PropTypes.func,
+ editTransaction: PropTypes.func,
historic: PropTypes.bool
};
@@ -50,6 +55,10 @@ class TxRow extends Component {
};
state = {
+ isCancelOpen: false,
+ isEditOpen: false,
+ canceled: false,
+ editing: false,
isContract: false,
isDeploy: false
};
@@ -166,11 +175,124 @@ class TxRow extends Component {
return (
{ blockNumber && block ? moment(block.timestamp).fromNow() : null }
- { blockNumber ? _blockNumber.toFormat() : 'Pending' }
+ { blockNumber ? _blockNumber.toFormat() : this.renderCancelToggle() }
|
);
}
+ renderCancelToggle () {
+ const { canceled, editing, isCancelOpen, isEditOpen } = this.state;
+
+ if (canceled) {
+ return (
+
+
+
+ );
+ }
+
+ if (editing) {
+ return (
+
+ );
+ }
+
+ if (!isCancelOpen && !isEditOpen) {
+ const pendingStatus = this.getCondition();
+
+ if (pendingStatus === 'submitting') {
+ return (
+
+ );
+ }
+ return (
+
+
+ { pendingStatus }
+
+
+
+
+
+
+
+
{' | '}
+
+
+
+
+ );
+ }
+
+ let which;
+
+ if (isCancelOpen) {
+ which = (
+
+ );
+ } else {
+ which = (
+
+ );
+ }
+
+ return (
+
+ );
+ }
+
getIsKnownContract (address) {
const { contractAddresses } = this.props;
@@ -194,6 +316,70 @@ class TxRow extends Component {
return `/addresses/${address}`;
}
+
+ getCondition = () => {
+ const { blockNumber, tx } = this.props;
+ let { time, block } = tx.condition;
+
+ if (time) {
+ if ((time.getTime() - Date.now()) >= 0) {
+ // return `${dateDifference(new Date(), time, { compact: true })} left`;
+ return (
+
+ );
+ } else {
+ return 'submitting';
+ }
+ } else if (blockNumber) {
+ block = blockNumber.minus(block);
+ // return (block.toNumber() < 0)
+ // ? block.abs().toFormat(0) + ' blocks left'
+ // : 'submitting';
+ if (block.toNumber() < 0) {
+ return (
+
+ );
+ } else {
+ return 'submitting';
+ }
+ }
+ }
+
+ cancelTx = () => {
+ const { cancelTransaction, tx } = this.props;
+
+ cancelTransaction(this, tx);
+ }
+
+ editTx = () => {
+ const { editTransaction, tx } = this.props;
+
+ editTransaction(this, tx);
+ }
+
+ setCancel = () => {
+ this.setState({ isCancelOpen: true });
+ }
+
+ setEdit = () => {
+ this.setState({ isEditOpen: true });
+ }
+
+ revertEditCancel = () => {
+ this.setState({ isCancelOpen: false, isEditOpen: false });
+ }
}
function mapStateToProps (initState) {
diff --git a/js/src/ui/TxList/store.js b/js/src/ui/TxList/store.js
index 1e670e31de9..99a081d57c1 100644
--- a/js/src/ui/TxList/store.js
+++ b/js/src/ui/TxList/store.js
@@ -14,150 +14,135 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-import { action, observable, transaction } from 'mobx';
-import { uniq } from 'lodash';
+import { action, observable } from 'mobx';
export default class Store {
@observable blocks = {};
@observable sortedHashes = [];
@observable transactions = {};
- constructor (api) {
+ constructor (api, onNewError, hashes) {
this._api = api;
- this._subscriptionId = 0;
- this._pendingHashes = [];
-
- this.subscribe();
+ this._onNewError = onNewError;
+ this.loadTransactions(hashes);
}
- @action addBlocks = (blocks) => {
- this.blocks = Object.assign({}, this.blocks, blocks);
+ @action addHash = (hash) => {
+ if (!this.sortedHashes.includes(hash)) {
+ this.sortedHashes.push(hash);
+ this.sortHashes();
+ }
}
- @action addTransactions = (transactions) => {
- transaction(() => {
- this.transactions = Object.assign({}, this.transactions, transactions);
- this.sortedHashes = Object
- .keys(this.transactions)
- .sort((ahash, bhash) => {
- const bnA = this.transactions[ahash].blockNumber;
- const bnB = this.transactions[bhash].blockNumber;
-
- if (bnB.eq(0)) {
- return bnB.eq(bnA) ? 0 : 1;
- } else if (bnA.eq(0)) {
- return -1;
- }
-
- return bnB.comparedTo(bnA);
- });
+ @action removeHash = (hash) => {
+ this.sortedHashes.remove(hash);
+ let tx = this.transactions[hash];
- this._pendingHashes = this.sortedHashes.filter((hash) => this.transactions[hash].blockNumber.eq(0));
- });
+ if (tx) {
+ delete this.transactions[hash];
+ delete this.blocks[tx.blockNumber];
+ }
+ this.sortHashes();
}
- @action clearPending () {
- this._pendingHashes = [];
+ containsAll = (arr1, arr2) => {
+ return arr2.every((arr2Item) => arr1.includes(arr2Item));
}
- subscribe () {
- this._api
- .subscribe('eth_blockNumber', (error, blockNumber) => {
- if (error) {
- return;
- }
-
- if (this._pendingHashes.length) {
- this.loadTransactions(this._pendingHashes);
- this.clearPending();
- }
- })
- .then((subscriptionId) => {
- this._subscriptionId = subscriptionId;
- });
+ sameHashList = (transactions) => {
+ return this.containsAll(transactions, this.sortedHashes) && this.containsAll(this.sortedHashes, transactions);
}
- unsubscribe () {
- if (!this._subscriptionId) {
- return;
- }
+ sortHashes = () => {
+ this.sortedHashes = this.sortedHashes.sort((hashA, hashB) => {
+ const bnA = this.transactions[hashA].blockNumber;
+ const bnB = this.transactions[hashB].blockNumber;
- this._api.unsubscribe(this._subscriptionId);
- this._subscriptionId = 0;
+ // 0 is a special case (has not been added to the blockchain yet)
+ if (bnB.eq(0)) {
+ return bnB.eq(bnA) ? 0 : 1;
+ } else if (bnA.eq(0)) {
+ return -1;
+ }
+
+ return bnB.comparedTo(bnA);
+ });
}
- loadTransactions (_txhashes = []) {
- const promises = _txhashes
- .filter((txhash) => !this.transactions[txhash] || this._pendingHashes.includes(txhash))
- .map((txhash) => {
- return Promise
- .all([
- this._api.eth.getTransactionByHash(txhash),
- this._api.eth.getTransactionReceipt(txhash)
- ])
- .then(([
- transaction = {},
- transactionReceipt = {}
- ]) => {
- return {
- ...transactionReceipt,
- ...transaction
- };
- });
- });
+ loadTransactions (_txhashes) {
+ const { eth } = this._api;
- if (!promises.length) {
+ // Ignore special cases and if the contents of _txhashes && this.sortedHashes are the same
+ if (Array.isArray(_txhashes) || this.sameHashList(_txhashes)) {
return;
}
- Promise
- .all(promises)
- .then((_transactions) => {
- const blockNumbers = [];
- const transactions = _transactions
- .filter((tx) => tx && tx.hash)
- .reduce((txs, tx) => {
- txs[tx.hash] = tx;
-
- if (tx.blockNumber && tx.blockNumber.gt(0)) {
- blockNumbers.push(tx.blockNumber.toNumber());
- }
-
- return txs;
- }, {});
-
- // No need to add transactions if there are none
- if (Object.keys(transactions).length === 0) {
- return false;
+ // Remove any tx that are edited/cancelled
+ this.sortedHashes
+ .forEach((hash) => {
+ if (!_txhashes.includes(hash)) {
+ this.removeHash(hash);
}
+ });
- this.addTransactions(transactions);
- this.loadBlocks(blockNumbers);
- })
- .catch((error) => {
- console.warn('loadTransactions', error);
+ // Add any new tx
+ _txhashes
+ .forEach((txhash) => {
+ if (this.sortedHashes.includes(txhash)) { return; }
+ eth.getTransactionByHash(txhash)
+ .then((tx) => {
+ if (!tx) { return; }
+ this.transactions[txhash] = tx;
+ // If the tx has a blockHash, let's get the blockNumber, otherwise it's ready to be added
+ if (tx.blockHash) {
+ eth.getBlockByNumber(tx.blockNumber)
+ .then((block) => {
+ this.blocks[tx.blockNumber] = block;
+ this.addHash(txhash);
+ });
+ } else {
+ this.addHash(txhash);
+ }
+ });
});
}
- loadBlocks (_blockNumbers) {
- const blockNumbers = uniq(_blockNumbers).filter((bn) => !this.blocks[bn]);
+ cancelTransaction = (txComponent, tx) => {
+ const { parity } = this._api;
+ const { hash } = tx;
- if (!blockNumbers || !blockNumbers.length) {
- return;
- }
+ parity
+ .removeTransaction(hash)
+ .then(() => {
+ txComponent.setState({ canceled: true });
+ })
+ .catch((err) => {
+ this._onNewError({ message: err });
+ });
+ }
- Promise
- .all(blockNumbers.map((blockNumber) => this._api.eth.getBlockByNumber(blockNumber)))
- .then((blocks) => {
- this.addBlocks(
- blocks.reduce((blocks, block, index) => {
- blocks[blockNumbers[index]] = block;
- return blocks;
- }, {})
- );
+ editTransaction = (txComponent, tx) => {
+ const { parity } = this._api;
+ const { hash, gas, gasPrice, to, from, value, input, condition } = tx;
+
+ parity
+ .removeTransaction(hash)
+ .then(() => {
+ parity.postTransaction({
+ from,
+ to,
+ gas,
+ gasPrice,
+ value,
+ condition,
+ data: input
+ });
+ })
+ .then(() => {
+ txComponent.setState({ editing: true });
})
- .catch((error) => {
- console.warn('loadBlocks', error);
+ .catch((err) => {
+ this._onNewError({ message: err });
});
}
}
diff --git a/js/src/ui/TxList/store.spec.js b/js/src/ui/TxList/store.spec.js
index 44685b3baaf..501065b956f 100644
--- a/js/src/ui/TxList/store.spec.js
+++ b/js/src/ui/TxList/store.spec.js
@@ -44,7 +44,7 @@ describe('ui/TxList/store', () => {
}
}
};
- store = new Store(api);
+ store = new Store(api, null, []);
});
describe('create', () => {
@@ -53,16 +53,14 @@ describe('ui/TxList/store', () => {
expect(store.sortedHashes.peek()).to.deep.equal([]);
expect(store.transactions).to.deep.equal({});
});
-
- it('subscribes to eth_blockNumber', () => {
- expect(api.subscribe).to.have.been.calledWith('eth_blockNumber');
- expect(store._subscriptionId).to.equal(SUBID);
- });
});
describe('addBlocks', () => {
beforeEach(() => {
- store.addBlocks(BLOCKS);
+ Object.keys(BLOCKS)
+ .forEach((blockNumber) => {
+ store.blocks[blockNumber] = BLOCKS[blockNumber];
+ });
});
it('adds the blocks to the list', () => {
@@ -72,7 +70,12 @@ describe('ui/TxList/store', () => {
describe('addTransactions', () => {
beforeEach(() => {
- store.addTransactions(TRANSACTIONS);
+ Object.keys(TRANSACTIONS)
+ .forEach((hash) => {
+ store.transactions[hash] = TRANSACTIONS[hash];
+ store.addHash(hash);
+ });
+ store.sortHashes();
});
it('adds all transactions to the list', () => {
@@ -82,9 +85,5 @@ describe('ui/TxList/store', () => {
it('sorts transactions based on blockNumber', () => {
expect(store.sortedHashes.peek()).to.deep.equal(['0x234', '0x456', '0x345', '0x123']);
});
-
- it('adds pending transactions to the pending queue', () => {
- expect(store._pendingHashes).to.deep.equal(['0x234', '0x456']);
- });
});
});
diff --git a/js/src/ui/TxList/txList.css b/js/src/ui/TxList/txList.css
index a38ba14fd14..0bfd898d27b 100644
--- a/js/src/ui/TxList/txList.css
+++ b/js/src/ui/TxList/txList.css
@@ -42,10 +42,11 @@
}
&.timestamp {
- padding-top: 1.5em;
- text-align: right;
+ max-width: 5em;
+ padding-top: 0.75em;
+ text-align: center;
line-height: 1.5em;
- opacity: 0.5;
+ color: grey;
}
&.transaction {
@@ -83,4 +84,16 @@
.left {
text-align: left;
}
+
+ .pending {
+ padding: 0em;
+ }
+
+ .pending div {
+ padding-bottom: 1.25em;
+ }
+
+ .uppercase {
+ text-transform: uppercase;
+ }
}
diff --git a/js/src/ui/TxList/txList.js b/js/src/ui/TxList/txList.js
index c2224903f99..140217a239c 100644
--- a/js/src/ui/TxList/txList.js
+++ b/js/src/ui/TxList/txList.js
@@ -35,17 +35,13 @@ class TxList extends Component {
PropTypes.array,
PropTypes.object
]).isRequired,
- netVersion: PropTypes.string.isRequired
+ blockNumber: PropTypes.object,
+ netVersion: PropTypes.string.isRequired,
+ onNewError: PropTypes.func
};
- store = new Store(this.context.api);
-
componentWillMount () {
- this.store.loadTransactions(this.props.hashes);
- }
-
- componentWillUnmount () {
- this.store.unsubscribe();
+ this.store = new Store(this.context.api, this.props.onNewError, this.props.hashes);
}
componentWillReceiveProps (newProps) {
@@ -63,20 +59,24 @@ class TxList extends Component {
}
renderRows () {
- const { address, netVersion } = this.props;
+ const { address, netVersion, blockNumber } = this.props;
+ const { editTransaction, cancelTransaction } = this.store;
return this.store.sortedHashes.map((txhash) => {
const tx = this.store.transactions[txhash];
- const blockNumber = tx.blockNumber.toNumber();
- const block = this.store.blocks[blockNumber];
+ const txBlockNumber = tx.blockNumber.toNumber();
+ const block = this.store.blocks[txBlockNumber];
return (
);
});
diff --git a/js/src/views/Signer/containers/RequestsPage/requestsPage.js b/js/src/views/Signer/containers/RequestsPage/requestsPage.js
index 327670a9d0d..8c2bf580f56 100644
--- a/js/src/views/Signer/containers/RequestsPage/requestsPage.js
+++ b/js/src/views/Signer/containers/RequestsPage/requestsPage.js
@@ -22,7 +22,8 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Store from '../../store';
-import * as RequestsActions from '~/redux/providers/signerActions';
+import { newError } from '~/redux/actions';
+import { startConfirmRequest, startRejectRequest } from '~/redux/providers/signerActions';
import { Container, Page, TxList } from '~/ui';
import RequestPending from '../../components/RequestPending';
@@ -36,12 +37,13 @@ class RequestsPage extends Component {
};
static propTypes = {
- actions: PropTypes.shape({
- startConfirmRequest: PropTypes.func.isRequired,
- startRejectRequest: PropTypes.func.isRequired
- }).isRequired,
gasLimit: PropTypes.object.isRequired,
netVersion: PropTypes.string.isRequired,
+ startConfirmRequest: PropTypes.func.isRequired,
+ startRejectRequest: PropTypes.func.isRequired,
+
+ blockNumber: PropTypes.object,
+ newError: PropTypes.func,
signer: PropTypes.shape({
pending: PropTypes.array.isRequired,
finished: PropTypes.array.isRequired
@@ -69,6 +71,7 @@ class RequestsPage extends Component {
renderLocalQueue () {
const { localHashes } = this.store;
+ const { blockNumber, newError } = this.props;
if (!localHashes.length) {
return null;
@@ -85,7 +88,9 @@ class RequestsPage extends Component {
>
);
@@ -114,7 +119,7 @@ class RequestsPage extends Component {
title={
}
>
@@ -124,7 +129,7 @@ class RequestsPage extends Component {
}
renderPending = (data, index) => {
- const { actions, gasLimit, netVersion } = this.props;
+ const { startConfirmRequest, startRejectRequest, gasLimit, netVersion } = this.props;
const { date, id, isSending, payload, origin } = data;
return (
@@ -137,8 +142,8 @@ class RequestsPage extends Component {
isSending={ isSending }
netVersion={ netVersion }
key={ id }
- onConfirm={ actions.startConfirmRequest }
- onReject={ actions.startRejectRequest }
+ onConfirm={ startConfirmRequest }
+ onReject={ startRejectRequest }
origin={ origin }
payload={ payload }
signerStore={ this.store }
@@ -148,11 +153,11 @@ class RequestsPage extends Component {
}
function mapStateToProps (state) {
- const { gasLimit, netVersion } = state.nodeStatus;
- const { actions, signer } = state;
+ const { gasLimit, netVersion, blockNumber } = state.nodeStatus;
+ const { signer } = state;
return {
- actions,
+ blockNumber,
gasLimit,
netVersion,
signer
@@ -160,9 +165,11 @@ function mapStateToProps (state) {
}
function mapDispatchToProps (dispatch) {
- return {
- actions: bindActionCreators(RequestsActions, dispatch)
- };
+ return bindActionCreators({
+ newError,
+ startConfirmRequest,
+ startRejectRequest
+ }, dispatch);
}
export default connect(
diff --git a/js/src/views/Signer/store.js b/js/src/views/Signer/store.js
index 76e3522f801..e9933315994 100644
--- a/js/src/views/Signer/store.js
+++ b/js/src/views/Signer/store.js
@@ -95,7 +95,11 @@ export default class SignerStore {
this._api.parity
.localTransactions()
.then((localTransactions) => {
- this.setLocalHashes(Object.keys(localTransactions));
+ const keys = Object
+ .keys(localTransactions)
+ .filter((key) => localTransactions[key].status !== 'canceled');
+
+ this.setLocalHashes(keys);
})
.then(nextTimeout)
.catch(nextTimeout);