diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d2c8f51d..b7c68bcad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- [#379](https://github.com/poanetwork/nifty-wallet/pull/379) - (Feature) Ability to set custom nonce of tx - [#377](https://github.com/poanetwork/nifty-wallet/pull/377) - (Fix) Sign message screen: do not decode message if it is not hex encoded - [#364](https://github.com/poanetwork/nifty-wallet/pull/364) - (Fix) notifications order in batch requests diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 91178b21d..19e7d60c7 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -290,7 +290,8 @@ class TransactionController extends EventEmitter { */ async updateAndApproveTransaction (txMeta) { this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction') - await this.approveTransaction(txMeta.id) + const customNonce = txMeta.txParams.nonce + await this.approveTransaction(txMeta.id, customNonce) } /** @@ -301,7 +302,7 @@ class TransactionController extends EventEmitter { if any of these steps fails the tx status will be set to failed @param txId {number} - the tx's Id */ - async approveTransaction (txId) { + async approveTransaction (txId, customNonce) { let nonceLock try { // approve @@ -315,7 +316,7 @@ class TransactionController extends EventEmitter { // if txMeta has lastGasPrice then it is a retry at same nonce with higher // gas price transaction and their for the nonce should not be calculated const nonce = txMeta.lastGasPrice ? txMeta.txParams.nonce : nonceLock.nextNonce - txMeta.txParams.nonce = ethUtil.addHexPrefix(nonce.toString(16)) + txMeta.txParams.nonce = customNonce || ethUtil.addHexPrefix(nonce.toString(16)) // add nonce debugging information to txMeta txMeta.nonceDetails = nonceLock.nonceDetails this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction') diff --git a/old-ui/app/components/send/send.js b/old-ui/app/components/send/send.js index edea83a32..93b531d8f 100644 --- a/old-ui/app/components/send/send.js +++ b/old-ui/app/components/send/send.js @@ -1,23 +1,220 @@ -const inherits = require('util').inherits -const PersistentForm = require('../../../lib/persistent-form') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../../../ui/app/actions') -const { +import React from 'react' +import PersistentForm from '../../../lib/persistent-form' +import { connect } from 'react-redux' +import actions from '../../../../ui/app/actions' +import { numericBalance, isHex, normalizeEthStringToWei, isInvalidChecksumAddress, isValidAddress, -} = require('../../util') -const EnsInput = require('../ens-input') -const ethUtil = require('ethereumjs-util') +} from '../../util' +import EnsInput from '../ens-input' +import ethUtil from 'ethereumjs-util' import SendProfile from './send-profile' import SendHeader from './send-header' import ErrorComponent from '../error' import { getMetaMaskAccounts } from '../../../../ui/app/selectors' import ToastComponent from '../toast' -module.exports = connect(mapStateToProps)(SendTransactionScreen) + +const optionalDataLabelStyle = { + background: '#ffffff', + color: '#333333', + marginTop: '16px', + marginBottom: '16px', +} +const optionalDataValueStyle = { + width: '100%', + resize: 'none', +} + +class SendTransactionScreen extends PersistentForm { + render () { + this.persistentFormParentId = 'send-tx-form' + + const props = this.props + const { + network, + identities, + addressBook, + error, + } = props + + return ( +
+ + + + + + + +
+ +
+ +
+ + + + + +
+ +

+ Transaction Data (optional) +

+ +
+ +
+ +

+ Custom nonce (optional) +

+ +
+ +
+
+ ) + } + + componentWillUnmount () { + this.props.dispatch(actions.displayWarning('')) + } + + navigateToAccounts (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) + } + + recipientDidChange (recipient, nickname) { + this.setState({ + recipient: recipient, + nickname: nickname, + }) + } + + onSubmit () { + const state = this.state || {} + let recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') + let nickname = state.nickname || ' ' + if (typeof recipient === 'object') { + if (recipient.toAddress) { + recipient = recipient.toAddress + } + if (recipient.nickname) { + nickname = recipient.nickname + } + } + const input = document.querySelector('input[name="amount"]').value + const parts = input.split('.') + + let message + + if (isNaN(input) || input === '') { + message = 'Invalid ether value.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (parts[1]) { + const decimal = parts[1] + if (decimal.length > 18) { + message = 'Ether amount is too precise.' + return this.props.dispatch(actions.displayWarning(message)) + } + } + + const value = normalizeEthStringToWei(input) + const txData = document.querySelector('input[name="txData"]').value + const txCustomNonce = document.querySelector('input[name="txCustomNonce"]').value + const balance = this.props.balance + + if (value.gt(balance)) { + message = 'Insufficient funds.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (input < 0) { + message = 'Can not send negative amounts of ETH.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if ((isInvalidChecksumAddress(recipient, this.props.network))) { + message = 'Recipient address checksum is invalid.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if ((!isValidAddress(recipient, this.props.network) && !txData) || (!recipient && !txData)) { + message = 'Recipient address is invalid.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { + message = 'Transaction data must be hex string.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.hideWarning()) + + this.props.dispatch(actions.addToAddressBook(recipient, nickname)) + + const txParams = { + from: this.props.address, + value: '0x' + value.toString(16), + } + + if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) + if (txData) txParams.data = txData + if (txCustomNonce) txParams.nonce = '0x' + parseInt(txCustomNonce, 10).toString(16) + + this.props.dispatch(actions.signTx(txParams)) + } +} function mapStateToProps (state) { const accounts = getMetaMaskAccounts(state) @@ -37,200 +234,4 @@ function mapStateToProps (state) { return result } -inherits(SendTransactionScreen, PersistentForm) -function SendTransactionScreen () { - PersistentForm.call(this) -} - -SendTransactionScreen.prototype.render = function () { - this.persistentFormParentId = 'send-tx-form' - - const props = this.props - const { - network, - identities, - addressBook, - error, - } = props - - return ( - - h('.send-screen.flex-column.flex-grow', [ - - h(ToastComponent, { - isSuccess: false, - }), - - // - // Sender Profile - // - - h(SendProfile), - - // - // Send Header - // - - h(SendHeader, { - title: 'Send Transaction', - }), - - // error message - h(ErrorComponent, { - error, - }), - - // 'to' field - h('section.flex-row.flex-center', [ - h(EnsInput, { - name: 'address', - placeholder: 'Recipient Address', - onChange: this.recipientDidChange.bind(this), - network, - identities, - addressBook, - }), - ]), - - // 'amount' and send button - h('section.flex-row.flex-center', [ - - h('input.large-input', { - name: 'amount', - placeholder: 'Amount', - type: 'number', - style: { - marginRight: '6px', - }, - dataset: { - persistentFormid: 'tx-amount', - }, - }), - - h('button', { - onClick: this.onSubmit.bind(this), - }, 'Next'), - - ]), - - // - // Optional Fields - // - h('h3.flex-center', { - style: { - background: '#ffffff', - color: '#333333', - marginTop: '16px', - marginBottom: '16px', - }, - }, [ - 'Transaction Data (optional)', - ]), - - // 'data' field - h('section.flex-column.flex-center', [ - h('input.large-input', { - name: 'txData', - placeholder: '0x01234', - style: { - width: '100%', - resize: 'none', - }, - dataset: { - persistentFormid: 'tx-data', - }, - }), - ]), - ]) - ) -} - -SendTransactionScreen.prototype.componentWillUnmount = function () { - this.props.dispatch(actions.displayWarning('')) -} - -SendTransactionScreen.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} - -SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { - this.setState({ - recipient: recipient, - nickname: nickname, - }) -} - -SendTransactionScreen.prototype.onSubmit = function () { - const state = this.state || {} - let recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') - let nickname = state.nickname || ' ' - if (typeof recipient === 'object') { - if (recipient.toAddress) { - recipient = recipient.toAddress - } - if (recipient.nickname) { - nickname = recipient.nickname - } - } - const input = document.querySelector('input[name="amount"]').value - const parts = input.split('.') - - let message - - if (isNaN(input) || input === '') { - message = 'Invalid ether value.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (parts[1]) { - const decimal = parts[1] - if (decimal.length > 18) { - message = 'Ether amount is too precise.' - return this.props.dispatch(actions.displayWarning(message)) - } - } - - const value = normalizeEthStringToWei(input) - const txData = document.querySelector('input[name="txData"]').value - const balance = this.props.balance - - if (value.gt(balance)) { - message = 'Insufficient funds.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (input < 0) { - message = 'Can not send negative amounts of ETH.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if ((isInvalidChecksumAddress(recipient, this.props.network))) { - message = 'Recipient address checksum is invalid.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if ((!isValidAddress(recipient, this.props.network) && !txData) || (!recipient && !txData)) { - message = 'Recipient address is invalid.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { - message = 'Transaction data must be hex string.' - return this.props.dispatch(actions.displayWarning(message)) - } - - this.props.dispatch(actions.hideWarning()) - - this.props.dispatch(actions.addToAddressBook(recipient, nickname)) - - const txParams = { - from: this.props.address, - value: '0x' + value.toString(16), - } - - if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) - if (txData) txParams.data = txData - - this.props.dispatch(actions.signTx(txParams)) -} +module.exports = connect(mapStateToProps)(SendTransactionScreen) diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index 4aa9d5f4a..dfcc64acc 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -345,7 +345,6 @@ describe('Transaction Controller', function () { to: '0x1678a085c290ebd122dc42cba69373b5953b831d', gasPrice: '0x77359400', gas: '0x7b0d', - nonce: '0x4b', }, metamaskNetworkId: currentNetworkId, } diff --git a/ui/app/actions.js b/ui/app/actions.js index 3c6ea9846..f89aebf4d 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -184,7 +184,6 @@ const actions = { cancelPersonalMsg, signTypedMsg, cancelTypedMsg, - sendTx: sendTx, signTx: signTx, signTokenTx: signTokenTx, updateTransaction, @@ -1198,22 +1197,6 @@ function clearSend () { } -function sendTx (txData) { - log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) - return (dispatch) => { - log.debug(`actions calling background.approveTransaction`) - background.approveTransaction(txData.id, (err) => { - if (err) { - err = err.message || err.error || err - dispatch(actions.txError(err)) - return log.error(err) - } - dispatch(actions.completedTx(txData.id)) - dispatch(actions.closeCurrentNotificationWindow()) - }) - } -} - function signTokenTx (tokenAddress, toAddress, amount, txData, confTxScreenParams) { return dispatch => { dispatch(actions.showLoadingIndication())