From f7d5d6c0cd4c53397bb8c49389a0ebcf9aebdf3c Mon Sep 17 00:00:00 2001 From: Craig O'Connor Date: Tue, 25 Apr 2017 04:08:09 -0400 Subject: [PATCH] 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 --- .gitignore | 3 + js/package.json | 1 + js/src/jsonrpc/interfaces/parity.js | 9 +- js/src/ui/MethodDecoding/methodDecoding.js | 36 +-- js/src/ui/TxList/TxRow/txRow.js | 188 +++++++++++++++- js/src/ui/TxList/store.js | 209 ++++++++---------- js/src/ui/TxList/store.spec.js | 23 +- js/src/ui/TxList/txList.css | 19 +- js/src/ui/TxList/txList.js | 22 +- .../containers/RequestsPage/requestsPage.js | 37 ++-- js/src/views/Signer/store.js | 6 +- 11 files changed, 381 insertions(+), 172 deletions(-) diff --git a/.gitignore b/.gitignore index f31145039..c36ae1c90 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 d6c83f075..f2e1f1770 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 e7a5f46c9..5d71c54df 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 1d2810c54..d143cd267 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 1fcf54396..c839a04b1 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 ( +
+
+
+ +
+ + { which } + + {' | '} + + + +
+ ); + } + 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 1e670e31d..99a081d57 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; + @action removeHash = (hash) => { + this.sortedHashes.remove(hash); + let tx = this.transactions[hash]; - if (bnB.eq(0)) { - return bnB.eq(bnA) ? 0 : 1; - } else if (bnA.eq(0)) { - return -1; - } + if (tx) { + delete this.transactions[hash]; + delete this.blocks[tx.blockNumber]; + } + this.sortHashes(); + } - return bnB.comparedTo(bnA); - }); + containsAll = (arr1, arr2) => { + return arr2.every((arr2Item) => arr1.includes(arr2Item)); + } - this._pendingHashes = this.sortedHashes.filter((hash) => this.transactions[hash].blockNumber.eq(0)); + 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)) { + return bnB.eq(bnA) ? 0 : 1; + } else if (bnA.eq(0)) { + return -1; + } + + return bnB.comparedTo(bnA); }); } - @action clearPending () { - this._pendingHashes = []; - } + loadTransactions (_txhashes) { + const { eth } = this._api; - 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; - }); - } - - unsubscribe () { - if (!this._subscriptionId) { + // Ignore special cases and if the contents of _txhashes && this.sortedHashes are the same + if (Array.isArray(_txhashes) || this.sameHashList(_txhashes)) { return; } - this._api.unsubscribe(this._subscriptionId); - this._subscriptionId = 0; - } + // Remove any tx that are edited/cancelled + this.sortedHashes + .forEach((hash) => { + if (!_txhashes.includes(hash)) { + this.removeHash(hash); + } + }); - 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 - }; + // 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); + } }); }); + } - if (!promises.length) { - return; - } + cancelTransaction = (txComponent, tx) => { + const { parity } = this._api; + const { hash } = tx; - 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); + parity + .removeTransaction(hash) + .then(() => { + txComponent.setState({ canceled: true }); }) - .catch((error) => { - console.warn('loadTransactions', error); + .catch((err) => { + this._onNewError({ message: err }); }); } - loadBlocks (_blockNumbers) { - const blockNumbers = uniq(_blockNumbers).filter((bn) => !this.blocks[bn]); + editTransaction = (txComponent, tx) => { + const { parity } = this._api; + const { hash, gas, gasPrice, to, from, value, input, condition } = tx; - 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; - }, {}) - ); + parity + .removeTransaction(hash) + .then(() => { + parity.postTransaction({ + from, + to, + gas, + gasPrice, + value, + condition, + data: input + }); }) - .catch((error) => { - console.warn('loadBlocks', error); + .then(() => { + txComponent.setState({ editing: true }); + }) + .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 44685b3ba..501065b95 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 a38ba14fd..0bfd898d2 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 c2224903f..140217a23 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 327670a9d..8c2bf580f 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 76e3522f8..e99333159 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);