diff --git a/CHANGELOG.md b/CHANGELOG.md index aa95fa1c3..6bea79aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ - [#4557](https://github.com/MetaMask/metamask-extension/pull/4557): Fix bug where nonce mutex was never released. - [#4558](https://github.com/MetaMask/metamask-extension/pull/4558): Stop reloading browser page on Ethereum network change. - [#4566](https://github.com/MetaMask/metamask-extension/pull/4566): Add phishing notice. +- Attempting to import an empty private key will now show a clear error. +- Fix bug where metamask data would stop being written to disk after prolonged use +- Fix bug where account reset did not work with custom RPC providers. +- Fix for Brave i18n getAcceptLanguages [#4270](https://github.com/MetaMask/metamask-extension/issues/4270) +- Fix bug where nonce mutex was never released +- Add phishing notice ## 4.7.4 Tue Jun 05 2018 diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 457c3c3b1..2579da87d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -146,6 +146,9 @@ "copy": { "message": "Copy" }, + "copyContractAddress": { + "message": "Copy Contract Address" + }, "copyToClipboard": { "message": "Copy to clipboard" }, @@ -955,6 +958,9 @@ "viewAccount": { "message": "View Account" }, + "viewOnEtherscan": { + "message": "View on Etherscan" + }, "visitWebSite": { "message": "Visit our web site" }, diff --git a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js index 84c6df1f0..e4df2367e 100644 --- a/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js +++ b/app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js @@ -1,4 +1,4 @@ -const Config = require('./recipient-blacklist-config.json') +const Config = require('./recipient-blacklist.js') /** @module*/ module.exports = { diff --git a/app/scripts/controllers/transactions/lib/recipient-blacklist-config.json b/app/scripts/controllers/transactions/lib/recipient-blacklist-config.json deleted file mode 100644 index b348eb72e..000000000 --- a/app/scripts/controllers/transactions/lib/recipient-blacklist-config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "blacklist": [ - "0x627306090abab3a6e1400e9345bc60c78a8bef57", - "0xf17f52151ebef6c7334fad080c5704d77216b732", - "0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef", - "0x821aea9a577a9b44299b9c15c88cf3087f3b5544", - "0x0d1d4e623d10f9fba5db95830f7d3839406c6af2", - "0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e", - "0x2191ef87e392377ec08e7c08eb105ef5448eced5", - "0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5", - "0x6330a553fc93768f612722bb8c2ec78ac90b3bbc", - "0x5aeda56215b167893e80b4fe645ba6d5bab767de" - ] -} diff --git a/app/scripts/controllers/transactions/lib/recipient-blacklist.js b/app/scripts/controllers/transactions/lib/recipient-blacklist.js new file mode 100644 index 000000000..08e1a2ccd --- /dev/null +++ b/app/scripts/controllers/transactions/lib/recipient-blacklist.js @@ -0,0 +1,17 @@ +module.exports = { + 'blacklist': [ + // IDEX phisher + '0x9bcb0A9d99d815Bb87ee3191b1399b1Bcc46dc77', + // Ganache default seed phrases + '0x627306090abab3a6e1400e9345bc60c78a8bef57', + '0xf17f52151ebef6c7334fad080c5704d77216b732', + '0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef', + '0x821aea9a577a9b44299b9c15c88cf3087f3b5544', + '0x0d1d4e623d10f9fba5db95830f7d3839406c6af2', + '0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e', + '0x2191ef87e392377ec08e7c08eb105ef5448eced5', + '0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5', + '0x6330a553fc93768f612722bb8c2ec78ac90b3bbc', + '0x5aeda56215b167893e80b4fe645ba6d5bab767de', + ], +} diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 070f5d247..7dd7fda02 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -3,6 +3,7 @@ cleanContextForImports() require('web3/dist/web3.min.js') const log = require('loglevel') const LocalMessageDuplexStream = require('post-message-stream') +const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('./lib/inpage-provider.js') restoreContextAfterImports() @@ -38,7 +39,11 @@ web3.setProvider = function () { } log.debug('MetaMask - injected web3') +setupDappAutoReload(web3, inpageProvider.publicConfigStore) + // export global web3, with usage-detection and deprecation warning + +/* TODO: Uncomment this area once auto-reload.js has been deprecated: let hasBeenWarned = false global.web3 = new Proxy(web3, { get: (_web3, key) => { @@ -55,6 +60,7 @@ global.web3 = new Proxy(web3, { _web3[key] = value }, }) +*/ // set web3 defaultAccount inpageProvider.publicConfigStore.subscribe(function (state) { diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js new file mode 100644 index 000000000..cce31c3d2 --- /dev/null +++ b/app/scripts/lib/auto-reload.js @@ -0,0 +1,61 @@ +module.exports = setupDappAutoReload + +function setupDappAutoReload (web3, observable) { + // export web3 as a global, checking for usage + let hasBeenWarned = false + let reloadInProgress = false + let lastTimeUsed + let lastSeenNetwork + + global.web3 = new Proxy(web3, { + get: (_web3, key) => { + // show warning once on web3 access + if (!hasBeenWarned && key !== 'currentProvider') { + console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation') + hasBeenWarned = true + } + // get the time of use + lastTimeUsed = Date.now() + // return value normally + return _web3[key] + }, + set: (_web3, key, value) => { + // set value normally + _web3[key] = value + }, + }) + + observable.subscribe(function (state) { + // if reload in progress, no need to check reload logic + if (reloadInProgress) return + + const currentNetwork = state.networkVersion + + // set the initial network + if (!lastSeenNetwork) { + lastSeenNetwork = currentNetwork + return + } + + // skip reload logic if web3 not used + if (!lastTimeUsed) return + + // if network did not change, exit + if (currentNetwork === lastSeenNetwork) return + + // initiate page reload + reloadInProgress = true + const timeSinceUse = Date.now() - lastTimeUsed + // if web3 was recently used then delay the reloading of the page + if (timeSinceUse > 500) { + triggerReset() + } else { + setTimeout(triggerReset, 500) + } + }) +} + +// reload the page +function triggerReset () { + global.location.reload() +} diff --git a/app/scripts/notice-controller.js b/app/scripts/notice-controller.js index e202043cf..2def4371e 100644 --- a/app/scripts/notice-controller.js +++ b/app/scripts/notice-controller.js @@ -13,12 +13,15 @@ module.exports = class NoticeController extends EventEmitter { this.firstVersion = opts.firstVersion this.version = opts.version const initState = extend({ - noticesList: this._filterNotices(hardCodedNotices), + noticesList: [], }, opts.initState) this.store = new ObservableStore(initState) + // setup memStore this.memStore = new ObservableStore({}) this.store.subscribe(() => this._updateMemstore()) this._updateMemstore() + // pull in latest notices + this.updateNoticesList() } getNoticesList () { @@ -84,8 +87,8 @@ module.exports = class NoticeController extends EventEmitter { } async _retrieveNoticeData () { - // Placeholder for the API. - return [] + // Placeholder for remote notice API. + return hardCodedNotices } _updateMemstore () { diff --git a/notices/archive/notice_4.md b/notices/archive/notice_4.md index 578c8488f..c7a5f83a9 100644 --- a/notices/archive/notice_4.md +++ b/notices/archive/notice_4.md @@ -1,6 +1,6 @@ Dear MetaMask Users, -There have been several instances of high-profile legitimate websites such as BTC Manager and Games Workshop that have had their websites temporarily compromised. This involves showing a fake MetaMask window on the page asking for user's seed phrases. MetaMask will never open itself in this way and users are encouraged to report these instances immediately to either [our phishing blacklist](https://github.com/MetaMask/eth-phishing-detect/issues) or our support email at [support@metamask.io](support@metamask.io). +There have been several instances of high-profile legitimate websites such as BTC Manager and Games Workshop that have had their websites temporarily compromised. This involves showing a fake MetaMask window on the page asking for user's seed phrases. MetaMask will never open itself in this way and users are encouraged to report these instances immediately to either [our phishing blacklist](https://github.com/MetaMask/eth-phishing-detect/issues) or our support email at [support@metamask.io](mailto:support@metamask.io). Please read our full article on this ongoing issue at [https://medium.com/metamask/new-phishing-strategy-becoming-common-1b1123837168](https://medium.com/metamask/new-phishing-strategy-becoming-common-1b1123837168). diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index 4d2ea2ea4..72e4a8cb1 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -117,12 +117,12 @@ async function runSendFlowTest(assert, done) { const sendGasField = await queryAsync($, '.send-v2__gas-fee-display') assert.equal( sendGasField.find('.currency-display__input-wrapper > input').val(), - '0.000198264', + '0.000021', 'send gas field should show estimated gas total' ) assert.equal( sendGasField.find('.currency-display__converted-value')[0].textContent, - '$0.24 USD', + '$0.03 USD', 'send gas field should show estimated gas total converted to USD' ) diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js index b70d0b893..fac7c451b 100644 --- a/ui/app/components/dropdowns/token-menu-dropdown.js +++ b/ui/app/components/dropdowns/token-menu-dropdown.js @@ -4,14 +4,21 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../actions') - +const genAccountLink = require('etherscan-link').createAccountLink +const copyToClipboard = require('copy-to-clipboard') +const { Menu, Item, CloseArea } = require('./components/menu') TokenMenuDropdown.contextTypes = { t: PropTypes.func, } -module.exports = connect(null, mapDispatchToProps)(TokenMenuDropdown) +module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenMenuDropdown) +function mapStateToProps (state) { + return { + network: state.metamask.network, + } +} function mapDispatchToProps (dispatch) { return { @@ -37,22 +44,34 @@ TokenMenuDropdown.prototype.onClose = function (e) { TokenMenuDropdown.prototype.render = function () { const { showHideTokenConfirmationModal } = this.props - return h('div.token-menu-dropdown', {}, [ - h('div.token-menu-dropdown__close-area', { + return h(Menu, { className: 'token-menu-dropdown', isShowing: true }, [ + h(CloseArea, { onClick: this.onClose, }), - h('div.token-menu-dropdown__container', {}, [ - h('div.token-menu-dropdown__options', {}, [ - - h('div.token-menu-dropdown__option', { - onClick: (e) => { - e.stopPropagation() - showHideTokenConfirmationModal(this.props.token) - this.props.onClose() - }, - }, this.context.t('hideToken')), - - ]), - ]), + h(Item, { + onClick: (e) => { + e.stopPropagation() + showHideTokenConfirmationModal(this.props.token) + this.props.onClose() + }, + text: this.context.t('hideToken'), + }), + h(Item, { + onClick: (e) => { + e.stopPropagation() + copyToClipboard(this.props.token.address) + this.props.onClose() + }, + text: this.context.t('copyContractAddress'), + }), + h(Item, { + onClick: (e) => { + e.stopPropagation() + const url = genAccountLink(this.props.token.address, this.props.network) + global.platform.openWindow({ url }) + this.props.onClose() + }, + text: this.context.t('viewOnEtherscan'), + }), ]) } diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index 3bc9ad226..e410bc070 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -57,6 +57,7 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi return selectedToken ? conversionUtil(ethUtil.addHexPrefix(value), { fromNumericBase: 'hex', + toNumericBase: 'dec', toCurrency: symbol, conversionRate: multiplier, invertConversionRate: true, @@ -91,8 +92,12 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu : convertedValue } +function removeLeadingZeroes (str) { + return str.replace(/^0*(?=\d)/, '') +} + CurrencyDisplay.prototype.handleChange = function (newVal) { - this.setState({ valueToRender: newVal }) + this.setState({ valueToRender: removeLeadingZeroes(newVal) }) this.props.onChange(this.getAmount(newVal)) } diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js index 8aefeed4a..8da36d3b7 100644 --- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js @@ -23,6 +23,7 @@ export default class SendAmountRow extends Component { tokenBalance: PropTypes.string, updateSendAmount: PropTypes.func, updateSendAmountError: PropTypes.func, + updateGas: PropTypes.func, } validateAmount (amount) { @@ -56,6 +57,14 @@ export default class SendAmountRow extends Component { updateSendAmount(amount) } + updateGas (amount) { + const { selectedToken, updateGas } = this.props + + if (selectedToken) { + updateGas({ amount }) + } + } + render () { const { amount, @@ -77,12 +86,15 @@ export default class SendAmountRow extends Component { this.updateAmount(newAmount)} + onBlur={newAmount => { + this.updateGas(newAmount) + this.updateAmount(newAmount) + }} onChange={newAmount => this.validateAmount(newAmount)} inError={inError} primaryCurrency={primaryCurrency || 'ETH'} selectedToken={selectedToken} - value={amount || '0x0'} + value={amount} /> ) diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js index 2205579ca..579e18585 100644 --- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js +++ b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js @@ -12,10 +12,12 @@ const propsMethodSpies = { setMaxModeTo: sinon.spy(), updateSendAmount: sinon.spy(), updateSendAmountError: sinon.spy(), + updateGas: sinon.spy(), } sinon.spy(SendAmountRow.prototype, 'updateAmount') sinon.spy(SendAmountRow.prototype, 'validateAmount') +sinon.spy(SendAmountRow.prototype, 'updateGas') describe('SendAmountRow Component', function () { let wrapper @@ -36,6 +38,7 @@ describe('SendAmountRow Component', function () { tokenBalance={'mockTokenBalance'} updateSendAmount={propsMethodSpies.updateSendAmount} updateSendAmountError={propsMethodSpies.updateSendAmountError} + updateGas={propsMethodSpies.updateGas} />, { context: { t: str => str + '_t' } }) instance = wrapper.instance() }) @@ -139,8 +142,14 @@ describe('SendAmountRow Component', function () { assert.equal(primaryCurrency, 'mockPrimaryCurrency') assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) assert.equal(value, 'mockAmount') + assert.equal(SendAmountRow.prototype.updateGas.callCount, 0) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0) onBlur('mockNewAmount') + assert.equal(SendAmountRow.prototype.updateGas.callCount, 1) + assert.deepEqual( + SendAmountRow.prototype.updateGas.getCall(0).args, + ['mockNewAmount'] + ) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1) assert.deepEqual( SendAmountRow.prototype.updateAmount.getCall(0).args, diff --git a/ui/app/components/send_/send-content/send-content.component.js b/ui/app/components/send_/send-content/send-content.component.js index 3a14054eb..adc114c0e 100644 --- a/ui/app/components/send_/send-content/send-content.component.js +++ b/ui/app/components/send_/send-content/send-content.component.js @@ -18,7 +18,7 @@ export default class SendContent extends Component {
this.props.updateGas(updateData)} /> - + this.props.updateGas(updateData)} />
diff --git a/ui/app/components/send_/send.component.js b/ui/app/components/send_/send.component.js index 516251e22..219b362f2 100644 --- a/ui/app/components/send_/send.component.js +++ b/ui/app/components/send_/send.component.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import PersistentForm from '../../../lib/persistent-form' import { getAmountErrorObject, + getToAddressForGasUpdate, doesAmountErrorRequireUpdate, } from './send.utils' @@ -38,7 +39,7 @@ export default class SendTransactionScreen extends PersistentForm { updateSendTokenBalance: PropTypes.func, }; - updateGas ({ to } = {}) { + updateGas ({ to: updatedToAddress, amount: value } = {}) { const { amount, blockGasLimit, @@ -48,6 +49,7 @@ export default class SendTransactionScreen extends PersistentForm { recentBlocks, selectedAddress, selectedToken = {}, + to: currentToAddress, updateAndSetGasTotal, } = this.props @@ -59,8 +61,8 @@ export default class SendTransactionScreen extends PersistentForm { recentBlocks, selectedAddress, selectedToken, - to: to && to.toLowerCase(), - value: amount, + to: getToAddressForGasUpdate(updatedToAddress, currentToAddress), + value: value || amount, }) } diff --git a/ui/app/components/send_/send.container.js b/ui/app/components/send_/send.container.js index 1fd96d61f..185653c5f 100644 --- a/ui/app/components/send_/send.container.js +++ b/ui/app/components/send_/send.container.js @@ -19,6 +19,7 @@ import { getSendAmount, getSendEditingTransactionId, getSendFromObject, + getSendTo, getTokenBalance, } from './send.selectors' import { @@ -54,6 +55,7 @@ function mapStateToProps (state) { recentBlocks: getRecentBlocks(state), selectedAddress: getSelectedAddress(state), selectedToken: getSelectedToken(state), + to: getSendTo(state), tokenBalance: getTokenBalance(state), tokenContract: getSelectedTokenContract(state), tokenToFiatRate: getSelectedTokenToFiatRate(state), diff --git a/ui/app/components/send_/send.utils.js b/ui/app/components/send_/send.utils.js index 67699be77..dfd459731 100644 --- a/ui/app/components/send_/send.utils.js +++ b/ui/app/components/send_/send.utils.js @@ -4,6 +4,7 @@ const { conversionGTE, multiplyCurrencies, conversionGreaterThan, + conversionLessThan, } = require('../../conversion-util') const { calcTokenAmount, @@ -20,6 +21,7 @@ const abi = require('ethereumjs-abi') const ethUtil = require('ethereumjs-util') module.exports = { + addGasBuffer, calcGasTotal, calcTokenBalance, doesAmountErrorRequireUpdate, @@ -27,6 +29,7 @@ module.exports = { estimateGasPriceFromRecentBlocks, generateTokenTransferData, getAmountErrorObject, + getToAddressForGasUpdate, isBalanceSufficient, isTokenBalanceSufficient, } @@ -175,9 +178,8 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to, } // if recipient has no code, gas is 21k max: - const hasRecipient = Boolean(to) - if (hasRecipient && !selectedToken) { - const code = await global.eth.getCode(to) + if (!selectedToken) { + const code = Boolean(to) && await global.eth.getCode(to) if (!code || code === '0x') { return SIMPLE_GAS_COST } @@ -201,16 +203,46 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to, err.message.includes('gas required exceeds allowance or always failing transaction') ) if (simulationFailed) { - return resolve(paramsForGasEstimate.gas) + const estimateWithBuffer = addGasBuffer(paramsForGasEstimate.gas, blockGasLimit, 1.5) + return resolve(ethUtil.addHexPrefix(estimateWithBuffer)) } else { return reject(err) } } - return resolve(estimatedGas.toString(16)) + const estimateWithBuffer = addGasBuffer(estimatedGas.toString(16), blockGasLimit, 1.5) + return resolve(ethUtil.addHexPrefix(estimateWithBuffer)) }) }) } +function addGasBuffer (initialGasLimitHex, blockGasLimitHex, bufferMultiplier = 1.5) { + const upperGasLimit = multiplyCurrencies(blockGasLimitHex, 0.9, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 10, + numberOfDecimals: '0', + }) + const bufferedGasLimit = multiplyCurrencies(initialGasLimitHex, bufferMultiplier, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 10, + numberOfDecimals: '0', + }) + + // if initialGasLimit is above blockGasLimit, dont modify it + if (conversionGreaterThan( + { value: initialGasLimitHex, fromNumericBase: 'hex' }, + { value: upperGasLimit, fromNumericBase: 'hex' }, + )) return initialGasLimitHex + // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit + if (conversionLessThan( + { value: bufferedGasLimit, fromNumericBase: 'hex' }, + { value: upperGasLimit, fromNumericBase: 'hex' }, + )) return bufferedGasLimit + // otherwise use blockGasLimit + return upperGasLimit +} + function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', selectedToken }) { if (!selectedToken) return return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( @@ -237,3 +269,7 @@ function estimateGasPriceFromRecentBlocks (recentBlocks) { return lowestPrices[Math.floor(lowestPrices.length / 2)] } + +function getToAddressForGasUpdate (...addresses) { + return [...addresses, ''].find(str => str !== undefined && str !== null).toLowerCase() +} diff --git a/ui/app/components/send_/tests/send-component.test.js b/ui/app/components/send_/tests/send-component.test.js index 4e33d8f63..4ba9b226d 100644 --- a/ui/app/components/send_/tests/send-component.test.js +++ b/ui/app/components/send_/tests/send-component.test.js @@ -201,7 +201,7 @@ describe('Send Component', function () { }) describe('updateGas', () => { - it('should call updateAndSetGasTotal with the correct params', () => { + it('should call updateAndSetGasTotal with the correct params if no to prop is passed', () => { propsMethodSpies.updateAndSetGasTotal.resetHistory() wrapper.instance().updateGas() assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1) @@ -215,12 +215,22 @@ describe('Send Component', function () { recentBlocks: ['mockBlock'], selectedAddress: 'mockSelectedAddress', selectedToken: 'mockSelectedToken', - to: undefined, + to: '', value: 'mockAmount', } ) }) + it('should call updateAndSetGasTotal with the correct params if a to prop is passed', () => { + propsMethodSpies.updateAndSetGasTotal.resetHistory() + wrapper.setProps({ to: 'someAddress' }) + wrapper.instance().updateGas() + assert.equal( + propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, + 'someaddress', + ) + }) + it('should call updateAndSetGasTotal with to set to lowercase if passed', () => { propsMethodSpies.updateAndSetGasTotal.resetHistory() wrapper.instance().updateGas({ to: '0xABC' }) diff --git a/ui/app/components/send_/tests/send-container.test.js b/ui/app/components/send_/tests/send-container.test.js index 056aad148..91484f4d8 100644 --- a/ui/app/components/send_/tests/send-container.test.js +++ b/ui/app/components/send_/tests/send-container.test.js @@ -39,6 +39,7 @@ proxyquire('../send.container.js', { getSelectedTokenContract: (s) => `mockTokenContract:${s}`, getSelectedTokenToFiatRate: (s) => `mockTokenToFiatRate:${s}`, getSendAmount: (s) => `mockAmount:${s}`, + getSendTo: (s) => `mockTo:${s}`, getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, getSendFromObject: (s) => `mockFrom:${s}`, getTokenBalance: (s) => `mockTokenBalance:${s}`, @@ -70,6 +71,7 @@ describe('send container', () => { recentBlocks: 'mockRecentBlocks:mockState', selectedAddress: 'mockSelectedAddress:mockState', selectedToken: 'mockSelectedToken:mockState', + to: 'mockTo:mockState', tokenBalance: 'mockTokenBalance:mockState', tokenContract: 'mockTokenContract:mockState', tokenToFiatRate: 'mockTokenToFiatRate:mockState', diff --git a/ui/app/components/send_/tests/send-utils.test.js b/ui/app/components/send_/tests/send-utils.test.js index b3f6372ef..f3d5674b7 100644 --- a/ui/app/components/send_/tests/send-utils.test.js +++ b/ui/app/components/send_/tests/send-utils.test.js @@ -18,10 +18,12 @@ const { const stubs = { addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b), conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)), - conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value), + conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value), multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`), calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d), rawEncode: sinon.stub().returns([16, 1100]), + conversionGreaterThan: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value), + conversionLessThan: sinon.stub().callsFake((obj1, obj2) => obj1.value < obj2.value), } const sendUtils = proxyquire('../send.utils.js', { @@ -30,6 +32,8 @@ const sendUtils = proxyquire('../send.utils.js', { conversionUtil: stubs.conversionUtil, conversionGTE: stubs.conversionGTE, multiplyCurrencies: stubs.multiplyCurrencies, + conversionGreaterThan: stubs.conversionGreaterThan, + conversionLessThan: stubs.conversionLessThan, }, '../../token-util': { calcTokenAmount: stubs.calcTokenAmount }, 'ethereumjs-abi': { @@ -44,6 +48,7 @@ const { estimateGasPriceFromRecentBlocks, generateTokenTransferData, getAmountErrorObject, + getToAddressForGasUpdate, calcTokenBalance, isBalanceSufficient, isTokenBalanceSufficient, @@ -255,7 +260,7 @@ describe('send utils', () => { estimateGasMethod: sinon.stub().callsFake( (data, cb) => cb( data.to.match(/willFailBecauseOf:/) ? { message: data.to.match(/:(.+)$/)[1] } : null, - { toString: (n) => `mockToString:${n}` } + { toString: (n) => `0xabc${n}` } ) ), } @@ -279,13 +284,23 @@ describe('send utils', () => { }) it('should call ethQuery.estimateGas with the expected params', async () => { - const result = await estimateGas(baseMockParams) + const result = await sendUtils.estimateGas(baseMockParams) assert.equal(baseMockParams.estimateGasMethod.callCount, 1) assert.deepEqual( baseMockParams.estimateGasMethod.getCall(0).args[0], Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall) ) - assert.equal(result, 'mockToString:16') + assert.equal(result, '0xabc16') + }) + + it('should call ethQuery.estimateGas with the expected params when initialGasLimitHex is lower than the upperGasLimit', async () => { + const result = await estimateGas(Object.assign({}, baseMockParams, { blockGasLimit: '0xbcd' })) + assert.equal(baseMockParams.estimateGasMethod.callCount, 1) + assert.deepEqual( + baseMockParams.estimateGasMethod.getCall(0).args[0], + Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall, { gas: '0xbcdx0.95' }) + ) + assert.equal(result, '0xabc16x1.5') }) it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => { @@ -300,7 +315,7 @@ describe('send utils', () => { to: 'mockAddress', }) ) - assert.equal(result, 'mockToString:16') + assert.equal(result, '0xabc16') }) it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => { @@ -309,6 +324,12 @@ describe('send utils', () => { assert.equal(result, SIMPLE_GAS_COST) }) + it(`should return ${SIMPLE_GAS_COST} if not passed a selectedToken or truthy to address`, async () => { + assert.equal(baseMockParams.estimateGasMethod.callCount, 0) + const result = await estimateGas(Object.assign({}, baseMockParams, { to: null })) + assert.equal(result, SIMPLE_GAS_COST) + }) + it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => { assert.equal(baseMockParams.estimateGasMethod.callCount, 0) const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } })) @@ -401,4 +422,15 @@ describe('send utils', () => { assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5') }) }) + + describe('getToAddressForGasUpdate()', () => { + it('should return empty string if all params are undefined or null', () => { + assert.equal(getToAddressForGasUpdate(undefined, null), '') + }) + + it('should return the first string that is not defined or null in lower case', () => { + assert.equal(getToAddressForGasUpdate('A', null), 'a') + assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b') + }) + }) }) diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 100402d95..337763067 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -190,6 +190,16 @@ const conversionGreaterThan = ( return firstValue.gt(secondValue) } +const conversionLessThan = ( + { ...firstProps }, + { ...secondProps }, +) => { + const firstValue = converter({ ...firstProps }) + const secondValue = converter({ ...secondProps }) + + return firstValue.lt(secondValue) +} + const conversionMax = ( { ...firstProps }, { ...secondProps }, @@ -229,6 +239,7 @@ module.exports = { addCurrencies, multiplyCurrencies, conversionGreaterThan, + conversionLessThan, conversionGTE, conversionLTE, conversionMax, diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss index 72fda372f..4b706abce 100644 --- a/ui/app/css/itcss/components/token-list.scss +++ b/ui/app/css/itcss/components/token-list.scss @@ -81,13 +81,9 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( } .token-menu-dropdown { - height: 55px; width: 80%; - border-radius: 4px; - background-color: rgba(0, 0, 0, .82); - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5); position: absolute; - top: 60px; + top: 52px; right: 25px; z-index: 2000;