Cancel tx JS (#4958)

* Remove transaction RPC

* Bumping multihash and libc

* Updating nanomsg

* bump nanomsg

* cancel tx

* cancel-tx-js

* cancel-tx-js

* cancel-tx-js

* cancel-tx-hs

* cancel-tx-js

* cancel-tx-js

* cancel-tx-js

* small fixes

* edit & time till submit

* edit & time till submit

* updates

* updates

* udpates

* udpates

* grumbles

* step 1

* Wonderful updates

* ready

* small refact

* small refact

* grumbles 1

* ffx2

* good ol' fashioned updates

* latest and greatest

* removeHash

* removeHash

* spec

* fix 1

* fix 1

* fix 2

* fix 2

* ff

* ff

* ff

* updates
This commit is contained in:
Craig O'Connor 2017-04-25 04:08:09 -04:00 committed by Jaco Greeff
parent 0768ce3600
commit f7d5d6c0cd
11 changed files with 381 additions and 172 deletions

3
.gitignore vendored
View File

@ -19,6 +19,9 @@
# mac stuff # mac stuff
.DS_Store .DS_Store
# npm stuff
npm-debug.log
# gdb files # gdb files
.gdb_history .gdb_history

View File

@ -164,6 +164,7 @@
"blockies": "0.0.2", "blockies": "0.0.2",
"brace": "0.9.0", "brace": "0.9.0",
"bytes": "2.4.0", "bytes": "2.4.0",
"date-difference": "1.0.0",
"debounce": "1.0.0", "debounce": "1.0.0",
"es6-error": "4.0.0", "es6-error": "4.0.0",
"es6-promise": "4.0.5", "es6-promise": "4.0.5",

View File

@ -1833,7 +1833,14 @@ export default {
example: { example: {
from: '0xb60e8dd61c5d32be8058bb8eb970870f07233155', from: '0xb60e8dd61c5d32be8058bb8eb970870f07233155',
to: '0xd46e8dd67c5d32be8058bb8eb970870f07244567', to: '0xd46e8dd67c5d32be8058bb8eb970870f07244567',
value: fromDecimal(2441406250) gas: fromDecimal(30400),
gasPrice: fromDecimal(10000000000000),
value: fromDecimal(2441406250),
data: '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675',
condition: {
block: 354221,
time: new Date()
}
} }
} }
], ],

View File

@ -185,14 +185,16 @@ class MethodDecoding extends Component {
); );
return ( return (
<div>
<FormattedMessage <FormattedMessage
id='ui.methodDecoding.condition.block' id='ui.methodDecoding.condition.block'
defaultMessage=', {historic, select, true {Submitted} false {Submission}} at block {blockNumber}' defaultMessage='{historic, select, true {Will be submitted} false {To be submitted}} at block {blockNumber}'
values={ { values={ {
historic, historic,
blockNumber blockNumber
} } } }
/> />
</div>
); );
} }
@ -204,14 +206,16 @@ class MethodDecoding extends Component {
); );
return ( return (
<div>
<FormattedMessage <FormattedMessage
id='ui.methodDecoding.condition.time' id='ui.methodDecoding.condition.time'
defaultMessage=', {historic, select, true {Submitted} false {Submission}} at {timestamp}' defaultMessage='{historic, select, true {Will be submitted} false {To be submitted}} {timestamp}'
values={ { values={ {
historic, historic,
timestamp timestamp
} } } }
/> />
</div>
); );
} }

View File

@ -15,6 +15,8 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import moment from 'moment'; import moment from 'moment';
import dateDifference from 'date-difference';
import { FormattedMessage } from 'react-intl';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
@ -36,12 +38,15 @@ class TxRow extends Component {
static propTypes = { static propTypes = {
accountAddresses: PropTypes.array.isRequired, accountAddresses: PropTypes.array.isRequired,
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
blockNumber: PropTypes.object,
contractAddresses: PropTypes.array.isRequired, contractAddresses: PropTypes.array.isRequired,
netVersion: PropTypes.string.isRequired, netVersion: PropTypes.string.isRequired,
tx: PropTypes.object.isRequired, tx: PropTypes.object.isRequired,
block: PropTypes.object, block: PropTypes.object,
className: PropTypes.string, className: PropTypes.string,
cancelTransaction: PropTypes.func,
editTransaction: PropTypes.func,
historic: PropTypes.bool historic: PropTypes.bool
}; };
@ -50,6 +55,10 @@ class TxRow extends Component {
}; };
state = { state = {
isCancelOpen: false,
isEditOpen: false,
canceled: false,
editing: false,
isContract: false, isContract: false,
isDeploy: false isDeploy: false
}; };
@ -166,11 +175,124 @@ class TxRow extends Component {
return ( return (
<td className={ styles.timestamp }> <td className={ styles.timestamp }>
<div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div> <div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div>
<div>{ blockNumber ? _blockNumber.toFormat() : 'Pending' }</div> <div>{ blockNumber ? _blockNumber.toFormat() : this.renderCancelToggle() }</div>
</td> </td>
); );
} }
renderCancelToggle () {
const { canceled, editing, isCancelOpen, isEditOpen } = this.state;
if (canceled) {
return (
<div className={ styles.pending }>
<FormattedMessage
lassName={ styles.uppercase }
id='ui.txList.txRow.canceled'
defaultMessage='Canceled'
/>
</div>
);
}
if (editing) {
return (
<div className={ styles.pending }>
<div className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.editing'
defaultMessage='Editing'
/>
</div>
</div>
);
}
if (!isCancelOpen && !isEditOpen) {
const pendingStatus = this.getCondition();
if (pendingStatus === 'submitting') {
return (
<div className={ styles.pending }>
<div />
<div className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.submitting'
defaultMessage='Submitting'
/>
</div>
</div>
);
}
return (
<div className={ styles.pending }>
<span>
{ pendingStatus }
</span>
<div className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.scheduled'
defaultMessage='Scheduled'
/>
</div>
<a onClick={ this.setEdit } className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.edit'
defaultMessage='Edit'
/>
</a>
<span>{' | '}</span>
<a onClick={ this.setCancel } className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.cancel'
defaultMessage='Cancel'
/>
</a>
</div>
);
}
let which;
if (isCancelOpen) {
which = (
<FormattedMessage
id='ui.txList.txRow.verify.cancelEditCancel'
defaultMessage='Cancel'
/>
);
} else {
which = (
<FormattedMessage
id='ui.txList.txRow.verify.cancelEditEdit'
defaultMessage='Edit'
/>
);
}
return (
<div className={ styles.pending }>
<div />
<div className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.verify'
defaultMessage='Are you sure?'
/>
</div>
<a onClick={ (isCancelOpen) ? this.cancelTx : this.editTx }>
{ which }
</a>
<span>{' | '}</span>
<a onClick={ this.revertEditCancel }>
<FormattedMessage
id='ui.txList.txRow.verify.nevermind'
defaultMessage='Nevermind'
/>
</a>
</div>
);
}
getIsKnownContract (address) { getIsKnownContract (address) {
const { contractAddresses } = this.props; const { contractAddresses } = this.props;
@ -194,6 +316,70 @@ class TxRow extends Component {
return `/addresses/${address}`; 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 (
<FormattedMessage
id='ui.txList.txRow.pendingStatus.time'
defaultMessage='{time} left'
values={ {
time: dateDifference(new Date(), time, { compact: true })
} }
/>
);
} 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 (
<FormattedMessage
id='ui.txList.txRow.pendingStatus.blocksLeft'
defaultMessage='{blockNumber} blocks left'
values={ {
blockNumber: block.abs().toFormat(0)
} }
/>
);
} 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) { function mapStateToProps (initState) {

View File

@ -14,35 +14,51 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, observable, transaction } from 'mobx'; import { action, observable } from 'mobx';
import { uniq } from 'lodash';
export default class Store { export default class Store {
@observable blocks = {}; @observable blocks = {};
@observable sortedHashes = []; @observable sortedHashes = [];
@observable transactions = {}; @observable transactions = {};
constructor (api) { constructor (api, onNewError, hashes) {
this._api = api; this._api = api;
this._subscriptionId = 0; this._onNewError = onNewError;
this._pendingHashes = []; this.loadTransactions(hashes);
this.subscribe();
} }
@action addBlocks = (blocks) => { @action addHash = (hash) => {
this.blocks = Object.assign({}, this.blocks, blocks); if (!this.sortedHashes.includes(hash)) {
this.sortedHashes.push(hash);
this.sortHashes();
}
} }
@action addTransactions = (transactions) => { @action removeHash = (hash) => {
transaction(() => { this.sortedHashes.remove(hash);
this.transactions = Object.assign({}, this.transactions, transactions); let tx = this.transactions[hash];
this.sortedHashes = Object
.keys(this.transactions)
.sort((ahash, bhash) => {
const bnA = this.transactions[ahash].blockNumber;
const bnB = this.transactions[bhash].blockNumber;
if (tx) {
delete this.transactions[hash];
delete this.blocks[tx.blockNumber];
}
this.sortHashes();
}
containsAll = (arr1, arr2) => {
return arr2.every((arr2Item) => arr1.includes(arr2Item));
}
sameHashList = (transactions) => {
return this.containsAll(transactions, this.sortedHashes) && this.containsAll(this.sortedHashes, transactions);
}
sortHashes = () => {
this.sortedHashes = this.sortedHashes.sort((hashA, hashB) => {
const bnA = this.transactions[hashA].blockNumber;
const bnB = this.transactions[hashB].blockNumber;
// 0 is a special case (has not been added to the blockchain yet)
if (bnB.eq(0)) { if (bnB.eq(0)) {
return bnB.eq(bnA) ? 0 : 1; return bnB.eq(bnA) ? 0 : 1;
} else if (bnA.eq(0)) { } else if (bnA.eq(0)) {
@ -51,113 +67,82 @@ export default class Store {
return bnB.comparedTo(bnA); return bnB.comparedTo(bnA);
}); });
this._pendingHashes = this.sortedHashes.filter((hash) => this.transactions[hash].blockNumber.eq(0));
});
} }
@action clearPending () { loadTransactions (_txhashes) {
this._pendingHashes = []; const { eth } = this._api;
}
subscribe () { // Ignore special cases and if the contents of _txhashes && this.sortedHashes are the same
this._api if (Array.isArray(_txhashes) || this.sameHashList(_txhashes)) {
.subscribe('eth_blockNumber', (error, blockNumber) => {
if (error) {
return; return;
} }
if (this._pendingHashes.length) { // Remove any tx that are edited/cancelled
this.loadTransactions(this._pendingHashes); this.sortedHashes
this.clearPending(); .forEach((hash) => {
if (!_txhashes.includes(hash)) {
this.removeHash(hash);
} }
});
// 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);
}
});
});
}
cancelTransaction = (txComponent, tx) => {
const { parity } = this._api;
const { hash } = tx;
parity
.removeTransaction(hash)
.then(() => {
txComponent.setState({ canceled: true });
}) })
.then((subscriptionId) => { .catch((err) => {
this._subscriptionId = subscriptionId; this._onNewError({ message: err });
}); });
} }
unsubscribe () { editTransaction = (txComponent, tx) => {
if (!this._subscriptionId) { const { parity } = this._api;
return; const { hash, gas, gasPrice, to, from, value, input, condition } = tx;
}
this._api.unsubscribe(this._subscriptionId); parity
this._subscriptionId = 0; .removeTransaction(hash)
} .then(() => {
parity.postTransaction({
loadTransactions (_txhashes = []) { from,
const promises = _txhashes to,
.filter((txhash) => !this.transactions[txhash] || this._pendingHashes.includes(txhash)) gas,
.map((txhash) => { gasPrice,
return Promise value,
.all([ condition,
this._api.eth.getTransactionByHash(txhash), data: input
this._api.eth.getTransactionReceipt(txhash)
])
.then(([
transaction = {},
transactionReceipt = {}
]) => {
return {
...transactionReceipt,
...transaction
};
}); });
});
if (!promises.length) {
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;
}
this.addTransactions(transactions);
this.loadBlocks(blockNumbers);
}) })
.catch((error) => { .then(() => {
console.warn('loadTransactions', error); txComponent.setState({ editing: true });
});
}
loadBlocks (_blockNumbers) {
const blockNumbers = uniq(_blockNumbers).filter((bn) => !this.blocks[bn]);
if (!blockNumbers || !blockNumbers.length) {
return;
}
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;
}, {})
);
}) })
.catch((error) => { .catch((err) => {
console.warn('loadBlocks', error); this._onNewError({ message: err });
}); });
} }
} }

View File

@ -44,7 +44,7 @@ describe('ui/TxList/store', () => {
} }
} }
}; };
store = new Store(api); store = new Store(api, null, []);
}); });
describe('create', () => { describe('create', () => {
@ -53,16 +53,14 @@ describe('ui/TxList/store', () => {
expect(store.sortedHashes.peek()).to.deep.equal([]); expect(store.sortedHashes.peek()).to.deep.equal([]);
expect(store.transactions).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', () => { describe('addBlocks', () => {
beforeEach(() => { beforeEach(() => {
store.addBlocks(BLOCKS); Object.keys(BLOCKS)
.forEach((blockNumber) => {
store.blocks[blockNumber] = BLOCKS[blockNumber];
});
}); });
it('adds the blocks to the list', () => { it('adds the blocks to the list', () => {
@ -72,7 +70,12 @@ describe('ui/TxList/store', () => {
describe('addTransactions', () => { describe('addTransactions', () => {
beforeEach(() => { 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', () => { it('adds all transactions to the list', () => {
@ -82,9 +85,5 @@ describe('ui/TxList/store', () => {
it('sorts transactions based on blockNumber', () => { it('sorts transactions based on blockNumber', () => {
expect(store.sortedHashes.peek()).to.deep.equal(['0x234', '0x456', '0x345', '0x123']); 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']);
});
}); });
}); });

View File

@ -42,10 +42,11 @@
} }
&.timestamp { &.timestamp {
padding-top: 1.5em; max-width: 5em;
text-align: right; padding-top: 0.75em;
text-align: center;
line-height: 1.5em; line-height: 1.5em;
opacity: 0.5; color: grey;
} }
&.transaction { &.transaction {
@ -83,4 +84,16 @@
.left { .left {
text-align: left; text-align: left;
} }
.pending {
padding: 0em;
}
.pending div {
padding-bottom: 1.25em;
}
.uppercase {
text-transform: uppercase;
}
} }

View File

@ -35,17 +35,13 @@ class TxList extends Component {
PropTypes.array, PropTypes.array,
PropTypes.object PropTypes.object
]).isRequired, ]).isRequired,
netVersion: PropTypes.string.isRequired blockNumber: PropTypes.object,
netVersion: PropTypes.string.isRequired,
onNewError: PropTypes.func
}; };
store = new Store(this.context.api);
componentWillMount () { componentWillMount () {
this.store.loadTransactions(this.props.hashes); this.store = new Store(this.context.api, this.props.onNewError, this.props.hashes);
}
componentWillUnmount () {
this.store.unsubscribe();
} }
componentWillReceiveProps (newProps) { componentWillReceiveProps (newProps) {
@ -63,20 +59,24 @@ class TxList extends Component {
} }
renderRows () { renderRows () {
const { address, netVersion } = this.props; const { address, netVersion, blockNumber } = this.props;
const { editTransaction, cancelTransaction } = this.store;
return this.store.sortedHashes.map((txhash) => { return this.store.sortedHashes.map((txhash) => {
const tx = this.store.transactions[txhash]; const tx = this.store.transactions[txhash];
const blockNumber = tx.blockNumber.toNumber(); const txBlockNumber = tx.blockNumber.toNumber();
const block = this.store.blocks[blockNumber]; const block = this.store.blocks[txBlockNumber];
return ( return (
<TxRow <TxRow
key={ tx.hash } key={ tx.hash }
tx={ tx } tx={ tx }
block={ block } block={ block }
blockNumber={ blockNumber }
address={ address } address={ address }
netVersion={ netVersion } netVersion={ netVersion }
editTransaction={ editTransaction }
cancelTransaction={ cancelTransaction }
/> />
); );
}); });

View File

@ -22,7 +22,8 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import Store from '../../store'; 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 { Container, Page, TxList } from '~/ui';
import RequestPending from '../../components/RequestPending'; import RequestPending from '../../components/RequestPending';
@ -36,12 +37,13 @@ class RequestsPage extends Component {
}; };
static propTypes = { static propTypes = {
actions: PropTypes.shape({
startConfirmRequest: PropTypes.func.isRequired,
startRejectRequest: PropTypes.func.isRequired
}).isRequired,
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
netVersion: PropTypes.string.isRequired, netVersion: PropTypes.string.isRequired,
startConfirmRequest: PropTypes.func.isRequired,
startRejectRequest: PropTypes.func.isRequired,
blockNumber: PropTypes.object,
newError: PropTypes.func,
signer: PropTypes.shape({ signer: PropTypes.shape({
pending: PropTypes.array.isRequired, pending: PropTypes.array.isRequired,
finished: PropTypes.array.isRequired finished: PropTypes.array.isRequired
@ -69,6 +71,7 @@ class RequestsPage extends Component {
renderLocalQueue () { renderLocalQueue () {
const { localHashes } = this.store; const { localHashes } = this.store;
const { blockNumber, newError } = this.props;
if (!localHashes.length) { if (!localHashes.length) {
return null; return null;
@ -85,7 +88,9 @@ class RequestsPage extends Component {
> >
<TxList <TxList
address='' address=''
blockNumber={ blockNumber }
hashes={ localHashes } hashes={ localHashes }
onNewError={ newError }
/> />
</Container> </Container>
); );
@ -114,7 +119,7 @@ class RequestsPage extends Component {
title={ title={
<FormattedMessage <FormattedMessage
id='signer.requestsPage.pendingTitle' id='signer.requestsPage.pendingTitle'
defaultMessage='Pending Requests' defaultMessage='Pending Signature Authorization'
/> />
} }
> >
@ -124,7 +129,7 @@ class RequestsPage extends Component {
} }
renderPending = (data, index) => { renderPending = (data, index) => {
const { actions, gasLimit, netVersion } = this.props; const { startConfirmRequest, startRejectRequest, gasLimit, netVersion } = this.props;
const { date, id, isSending, payload, origin } = data; const { date, id, isSending, payload, origin } = data;
return ( return (
@ -137,8 +142,8 @@ class RequestsPage extends Component {
isSending={ isSending } isSending={ isSending }
netVersion={ netVersion } netVersion={ netVersion }
key={ id } key={ id }
onConfirm={ actions.startConfirmRequest } onConfirm={ startConfirmRequest }
onReject={ actions.startRejectRequest } onReject={ startRejectRequest }
origin={ origin } origin={ origin }
payload={ payload } payload={ payload }
signerStore={ this.store } signerStore={ this.store }
@ -148,11 +153,11 @@ class RequestsPage extends Component {
} }
function mapStateToProps (state) { function mapStateToProps (state) {
const { gasLimit, netVersion } = state.nodeStatus; const { gasLimit, netVersion, blockNumber } = state.nodeStatus;
const { actions, signer } = state; const { signer } = state;
return { return {
actions, blockNumber,
gasLimit, gasLimit,
netVersion, netVersion,
signer signer
@ -160,9 +165,11 @@ function mapStateToProps (state) {
} }
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return bindActionCreators({
actions: bindActionCreators(RequestsActions, dispatch) newError,
}; startConfirmRequest,
startRejectRequest
}, dispatch);
} }
export default connect( export default connect(

View File

@ -95,7 +95,11 @@ export default class SignerStore {
this._api.parity this._api.parity
.localTransactions() .localTransactions()
.then((localTransactions) => { .then((localTransactions) => {
this.setLocalHashes(Object.keys(localTransactions)); const keys = Object
.keys(localTransactions)
.filter((key) => localTransactions[key].status !== 'canceled');
this.setLocalHashes(keys);
}) })
.then(nextTimeout) .then(nextTimeout)
.catch(nextTimeout); .catch(nextTimeout);