diff --git a/CHANGELOG.md b/CHANGELOG.md index bcdfe7e04..d18f195c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Current Develop Branch +## 7.7.4 Wed Jan 29 2020 +- [#7918](https://github.com/MetaMask/metamask-extension/pull/7918): Update data on Approve screen after updating custom spend limit +- [#7919](https://github.com/MetaMask/metamask-extension/pull/7919): Allow editing max spend limit +- [#7920](https://github.com/MetaMask/metamask-extension/pull/7920): Validate custom spend limit +- [#7944](https://github.com/MetaMask/metamask-extension/pull/7944): Only resolve ENS on mainnet +- [#7954](https://github.com/MetaMask/metamask-extension/pull/7954): Update ENS registry addresses + ## 7.7.3 Fri Jan 24 2020 - [#7894](https://github.com/MetaMask/metamask-extension/pull/7894): Update GABA dependency version - [#7901](https://github.com/MetaMask/metamask-extension/pull/7901): Use eth-contract-metadata@1.12.1 diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f947c9e5c..c9e1b02db 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1261,6 +1261,12 @@ "message": "Spend limit requested by $1", "description": "Origin of the site requesting the spend limit" }, + "spendLimitTooLarge": { + "message": "Spend limit too large" + }, + "spendLimitInvalid": { + "message": "Spend limit invalid; must be a positive number" + }, "switchNetworks": { "message": "Switch Networks" }, diff --git a/app/manifest.json b/app/manifest.json index b646d3568..2c6cbc644 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "7.7.3", + "version": "7.7.4", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", diff --git a/app/scripts/background.js b/app/scripts/background.js index 2639d7703..31c8d1815 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -251,7 +251,10 @@ function setupController (initState, initLangCode) { }) const provider = controller.provider - setupEnsIpfsResolver({ provider }) + setupEnsIpfsResolver({ + getCurrentNetwork: controller.getCurrentNetwork, + provider, + }) // submit rpc requests to mesh-metrics controller.networkController.on('rpc-req', (data) => { diff --git a/app/scripts/lib/ens-ipfs/resolver.js b/app/scripts/lib/ens-ipfs/resolver.js index a0af263bc..09e7b5b32 100644 --- a/app/scripts/lib/ens-ipfs/resolver.js +++ b/app/scripts/lib/ens-ipfs/resolver.js @@ -52,19 +52,23 @@ function hexValueIsEmpty (value) { return [undefined, null, '0x', '0x0', '0x0000000000000000000000000000000000000000000000000000000000000000'].includes(value) } +/** + * Returns the registry address for the given chain ID + * @param {number} chainId the chain ID + * @returns {string|null} the registry address if known, null otherwise + */ function getRegistryForChainId (chainId) { switch (chainId) { - // mainnet case 1: - return '0x314159265dd8dbb310642f98f50c066173c1259b' - // ropsten + // falls through case 3: - return '0x112234455c3a32fd11230c42e7bccd4a84e02010' - // rinkeby + // falls through case 4: - return '0xe7410170f87102df0055eb195163a03b7f2bff4a' - // goerli + // falls through case 5: - return '0x112234455c3a32fd11230c42e7bccd4a84e02010' + // Mainnet, Ropsten, Rinkeby, and Goerli, respectively, use the same address + return '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e' + default: + return null } } diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js index 8f19510c7..e4eddd494 100644 --- a/app/scripts/lib/ens-ipfs/setup.js +++ b/app/scripts/lib/ens-ipfs/setup.js @@ -6,7 +6,7 @@ const supportedTopLevelDomains = ['eth'] module.exports = setupEnsIpfsResolver -function setupEnsIpfsResolver ({ provider }) { +function setupEnsIpfsResolver ({ provider, getCurrentNetwork }) { // install listener const urlPatterns = supportedTopLevelDomains.map(tld => `*://*.${tld}/*`) @@ -23,7 +23,10 @@ function setupEnsIpfsResolver ({ provider }) { async function webRequestDidFail (details) { const { tabId, url } = details // ignore requests that are not associated with tabs - if (tabId === -1) return + // only attempt ENS resolution on mainnet + if (tabId === -1 || getCurrentNetwork() !== '1') { + return + } // parse ens name const urlData = urlUtil.parse(url) const { hostname: name, path, search, hash: fragment } = urlData diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 9a43b8a80..15e1d6aa2 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -675,6 +675,10 @@ module.exports = class MetamaskController extends EventEmitter { }) } + getCurrentNetwork = () => { + return this.networkController.store.getState().network + } + /** * Collects all the information that we want to share * with the mobile client for syncing purposes diff --git a/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js b/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js index 53ff473e4..f627ddaef 100644 --- a/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js +++ b/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js @@ -1,12 +1,18 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' +import log from 'loglevel' import Modal from '../../modal' import Identicon from '../../../ui/identicon' import TextField from '../../../ui/text-field' +import { calcTokenAmount } from '../../../../helpers/utils/token-util' import classnames from 'classnames' +import BigNumber from 'bignumber.js' + +const MAX_UNSIGNED_256_INT = new BigNumber(2).pow(256).minus(1).toString(10) export default class EditApprovalPermission extends PureComponent { static propTypes = { + decimals: PropTypes.number, hideModal: PropTypes.func.isRequired, selectedIdentity: PropTypes.object, tokenAmount: PropTypes.string, @@ -14,7 +20,7 @@ export default class EditApprovalPermission extends PureComponent { tokenSymbol: PropTypes.string, tokenBalance: PropTypes.string, setCustomAmount: PropTypes.func, - origin: PropTypes.string, + origin: PropTypes.string.isRequired, } static contextTypes = { @@ -26,7 +32,7 @@ export default class EditApprovalPermission extends PureComponent { selectedOptionIsUnlimited: !this.props.customTokenAmount, } - renderModalContent () { + renderModalContent (error) { const { t } = this.context const { hideModal, @@ -61,7 +67,7 @@ export default class EditApprovalPermission extends PureComponent {
{ t('balance') }
- {`${tokenBalance} ${tokenSymbol}`} + {`${Number(tokenBalance).toPrecision(9)} ${tokenSymbol}`}
@@ -89,7 +95,7 @@ export default class EditApprovalPermission extends PureComponent { 'edit-approval-permission__edit-section__option-label--selected': selectedOptionIsUnlimited, })}> { - tokenAmount < tokenBalance + (new BigNumber(tokenAmount)).lessThan(new BigNumber(tokenBalance)) ? t('proposedApprovalLimit') : t('unlimited') } @@ -98,7 +104,7 @@ export default class EditApprovalPermission extends PureComponent { { t('spendLimitRequestedBy', [origin]) }
- {`${tokenAmount} ${tokenSymbol}`} + {`${Number(tokenAmount)} ${tokenSymbol}`}
@@ -127,8 +133,7 @@ export default class EditApprovalPermission extends PureComponent {
{ this.setState({ customSpendLimit: event.target.value }) if (selectedOptionIsUnlimited) { @@ -138,6 +143,7 @@ export default class EditApprovalPermission extends PureComponent { fullWidth margin="dense" value={ this.state.customSpendLimit } + error={error} />
@@ -147,10 +153,44 @@ export default class EditApprovalPermission extends PureComponent { ) } + validateSpendLimit () { + const { t } = this.context + const { decimals } = this.props + const { selectedOptionIsUnlimited, customSpendLimit } = this.state + + if (selectedOptionIsUnlimited || !customSpendLimit) { + return + } + + let customSpendLimitNumber + try { + customSpendLimitNumber = new BigNumber(customSpendLimit) + } catch (error) { + log.debug(`Error converting '${customSpendLimit}' to BigNumber:`, error) + return t('spendLimitInvalid') + } + + if (customSpendLimitNumber.isNegative()) { + return t('spendLimitInvalid') + } + + const maxTokenAmount = calcTokenAmount(MAX_UNSIGNED_256_INT, decimals) + if (customSpendLimitNumber.greaterThan(maxTokenAmount)) { + return t('spendLimitTooLarge') + } + } + render () { const { t } = this.context const { setCustomAmount, hideModal, customTokenAmount } = this.props const { selectedOptionIsUnlimited, customSpendLimit } = this.state + + const error = this.validateSpendLimit() + const disabled = Boolean( + (customSpendLimit === customTokenAmount && !selectedOptionIsUnlimited) || + error + ) + return ( { @@ -161,9 +201,9 @@ export default class EditApprovalPermission extends PureComponent { submitType="primary" contentClass="edit-approval-permission-modal-content" containerClass="edit-approval-permission-modal-container" - submitDisabled={ (customSpendLimit === customTokenAmount) && !selectedOptionIsUnlimited } + submitDisabled={disabled} > - { this.renderModalContent() } + { this.renderModalContent(error) } ) } diff --git a/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index 38644541d..75608c16c 100644 --- a/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -15,6 +15,7 @@ export default class ConfirmApproveContent extends Component { static propTypes = { amount: PropTypes.string, txFeeTotal: PropTypes.string, + decimals: PropTypes.number, tokenAmount: PropTypes.string, customTokenAmount: PropTypes.string, tokenSymbol: PropTypes.string, @@ -100,7 +101,7 @@ export default class ConfirmApproveContent extends Component {
{ t('accessAndSpendNotice', [origin]) }
{ t('amountWithColon') }
-
{ `${customTokenAmount || tokenAmount} ${tokenSymbol}` }
+
{ `${Number(customTokenAmount || tokenAmount)} ${tokenSymbol}` }
{ t('toWithColon') }
@@ -124,6 +125,7 @@ export default class ConfirmApproveContent extends Component { render () { const { t } = this.context const { + decimals, siteImage, tokenAmount, customTokenAmount, @@ -159,7 +161,15 @@ export default class ConfirmApproveContent extends Component { >
showEditApprovalPermissionModal({ customTokenAmount, tokenAmount, tokenSymbol, setCustomAmount, tokenBalance, origin })} + onClick={() => showEditApprovalPermissionModal({ + customTokenAmount, + decimals, + origin, + setCustomAmount, + tokenAmount, + tokenSymbol, + tokenBalance, + })} > { t('editPermission') }
@@ -201,10 +211,12 @@ export default class ConfirmApproveContent extends Component { showEdit: true, onEditClick: () => showEditApprovalPermissionModal({ customTokenAmount, + decimals, + origin, + setCustomAmount, tokenAmount, tokenSymbol, tokenBalance, - setCustomAmount, }), })}
diff --git a/ui/app/pages/confirm-approve/confirm-approve.component.js b/ui/app/pages/confirm-approve/confirm-approve.component.js index e8c44cd4f..786a854e7 100644 --- a/ui/app/pages/confirm-approve/confirm-approve.component.js +++ b/ui/app/pages/confirm-approve/confirm-approve.component.js @@ -15,7 +15,7 @@ export default class ConfirmApprove extends Component { static propTypes = { tokenAddress: PropTypes.string, toAddress: PropTypes.string, - tokenAmount: PropTypes.number, + tokenAmount: PropTypes.string, tokenSymbol: PropTypes.string, fiatTransactionTotal: PropTypes.string, ethTransactionTotal: PropTypes.string, @@ -33,7 +33,7 @@ export default class ConfirmApprove extends Component { } static defaultProps = { - tokenAmount: 0, + tokenAmount: '0', } state = { @@ -69,12 +69,16 @@ export default class ConfirmApprove extends Component { } = this.props const { customPermissionAmount } = this.state - const tokensText = `${tokenAmount} ${tokenSymbol}` + const tokensText = `${Number(tokenAmount)} ${tokenSymbol}` const tokenBalance = tokenTrackerBalance - ? Number(calcTokenAmount(tokenTrackerBalance, decimals)).toPrecision(9) + ? calcTokenAmount(tokenTrackerBalance, decimals).toString(10) : '' + const customData = customPermissionAmount + ? getCustomTxParamsData(data, { customPermissionAmount, decimals }) + : null + return ( { this.setState({ customPermissionAmount: newAmount }) }} customTokenAmount={String(customPermissionAmount)} - tokenAmount={String(tokenAmount)} + tokenAmount={tokenAmount} origin={origin} tokenSymbol={tokenSymbol} tokenBalance={tokenBalance} showCustomizeGasModal={() => showCustomizeGasModal(txData)} showEditApprovalPermissionModal={showEditApprovalPermissionModal} - data={data} + data={customData || data} toAddress={toAddress} currentCurrency={currentCurrency} ethTransactionTotal={ethTransactionTotal} fiatTransactionTotal={fiatTransactionTotal} />} hideSenderToRecipient - customTxParamsData={customPermissionAmount - ? getCustomTxParamsData(data, { customPermissionAmount, tokenAmount, decimals }) - : null - } + customTxParamsData={customData} {...restProps} /> ) diff --git a/ui/app/pages/confirm-approve/confirm-approve.container.js b/ui/app/pages/confirm-approve/confirm-approve.container.js index 43f5aab90..185c0f9c6 100644 --- a/ui/app/pages/confirm-approve/confirm-approve.container.js +++ b/ui/app/pages/confirm-approve/confirm-approve.container.js @@ -43,7 +43,7 @@ const mapStateToProps = (state, ownProps) => { const tokenData = getTokenData(data) const tokenValue = tokenData && getTokenValue(tokenData.params) const toAddress = tokenData && getTokenToAddress(tokenData.params) - const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toNumber() + const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toString(10) const contractExchangeRate = contractExchangeRateSelector(state) const { origin } = transaction @@ -76,20 +76,22 @@ const mapDispatchToProps = (dispatch) => { return { showCustomizeGasModal: (txData) => dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData })), showEditApprovalPermissionModal: ({ - tokenAmount, customTokenAmount, - tokenSymbol, - tokenBalance, - setCustomAmount, + decimals, origin, + setCustomAmount, + tokenAmount, + tokenBalance, + tokenSymbol, }) => dispatch(showModal({ name: 'EDIT_APPROVAL_PERMISSION', - tokenAmount, customTokenAmount, - tokenSymbol, - tokenBalance, - setCustomAmount, + decimals, origin, + setCustomAmount, + tokenAmount, + tokenBalance, + tokenSymbol, })), } } diff --git a/ui/app/pages/confirm-approve/confirm-approve.util.js b/ui/app/pages/confirm-approve/confirm-approve.util.js index be77c65f9..0318c6bed 100644 --- a/ui/app/pages/confirm-approve/confirm-approve.util.js +++ b/ui/app/pages/confirm-approve/confirm-approve.util.js @@ -1,28 +1,33 @@ import { decimalToHex } from '../../helpers/utils/conversions.util' import { calcTokenValue } from '../../helpers/utils/token-util.js' +import { getTokenData } from '../../helpers/utils/transactions.util' -export function getCustomTxParamsData (data, { customPermissionAmount, tokenAmount, decimals }) { - if (customPermissionAmount) { - const tokenValue = decimalToHex(calcTokenValue(tokenAmount, decimals)) +export function getCustomTxParamsData (data, { customPermissionAmount, decimals }) { + const tokenData = getTokenData(data) - const re = new RegExp('(^.+)' + tokenValue + '$') - const matches = re.exec(data) - - if (!matches || !matches[1]) { - return data - } - let dataWithoutCurrentAmount = matches[1] - const customPermissionValue = decimalToHex(calcTokenValue(Number(customPermissionAmount), decimals)) + if (!tokenData) { + throw new Error('Invalid data') + } else if (tokenData.name !== 'approve') { + throw new Error(`Invalid data; should be 'approve' method, but instead is '${tokenData.name}'`) + } + let spender = tokenData.params[0].value + if (spender.startsWith('0x')) { + spender = spender.substring(2) + } + const [signature, tokenValue] = data.split(spender) - const differenceInLengths = customPermissionValue.length - tokenValue.length - const zeroModifier = dataWithoutCurrentAmount.length - differenceInLengths - if (differenceInLengths > 0) { - dataWithoutCurrentAmount = dataWithoutCurrentAmount.slice(0, zeroModifier) - } else if (differenceInLengths < 0) { - dataWithoutCurrentAmount = dataWithoutCurrentAmount.padEnd(zeroModifier, 0) - } + if (!signature || !tokenValue) { + throw new Error('Invalid data') + } else if (tokenValue.length !== 64) { + throw new Error('Invalid token value; should be exactly 64 hex digits long (u256)') + } - const customTxParamsData = dataWithoutCurrentAmount + customPermissionValue - return customTxParamsData + let customPermissionValue = decimalToHex(calcTokenValue(customPermissionAmount, decimals)) + if (customPermissionValue.length > 64) { + throw new Error('Custom value is larger than u256') } + + customPermissionValue = customPermissionValue.padStart(tokenValue.length, '0') + const customTxParamsData = `${signature}${spender}${customPermissionValue}` + return customTxParamsData } diff --git a/yarn.lock b/yarn.lock index 74d5eb8f6..5f03bb146 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7576,7 +7576,7 @@ cross-spawn@^4: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^5.0.1, cross-spawn@^5.1.0: +cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= @@ -10370,9 +10370,9 @@ ethereum-common@^0.0.18: integrity sha1-L9w1dvIykDNYl26znaeDIT/5Uj8= ethereum-ens-network-map@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.0.tgz#43cd7669ce950a789e151001118d4d65f210eeb7" - integrity sha1-Q812ac6VCnieFRABEY1NZfIQ7rc= + version "1.0.2" + resolved "https://registry.yarnpkg.com/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.2.tgz#4e27bad18dae7bd95d84edbcac2c9e739fc959b9" + integrity sha512-5qwJ5n3YhjSpE6O/WEBXCAb2nagUgyagJ6C0lGUBWC4LjKp/rRzD+pwtDJ6KCiITFEAoX4eIrWOjRy0Sylq5Hg== ethereumjs-abi@0.6.5, ethereumjs-abi@^0.6.4, ethereumjs-abi@^0.6.5: version "0.6.5"