diff --git a/test/unit/util_test.js b/test/unit/util_test.js index 87f57b218ef7..10bfc6c1a35a 100644 --- a/test/unit/util_test.js +++ b/test/unit/util_test.js @@ -103,6 +103,64 @@ describe('util', function () { }) }) + describe('isValidDomainName', function () { + it('should return true when given a valid domain name', function () { + assert.strictEqual(util.isValidDomainName('foo.bar'), true) + }) + + it('should return true when given a valid subdomain', function () { + assert.strictEqual(util.isValidDomainName('foo.foo.bar'), true) + }) + + it('should return true when given a single-character domain', function () { + assert.strictEqual(util.isValidDomainName('f.bar'), true) + }) + + it('should return true when given a unicode TLD', function () { + assert.strictEqual(util.isValidDomainName('台灣.中国'), true) + }) + + it('should return false when given a domain with unacceptable ASCII characters', function () { + assert.strictEqual(util.isValidDomainName('$.bar'), false) + }) + + it('should return false when given a TLD that starts with a dash', function () { + assert.strictEqual(util.isValidDomainName('foo.-bar'), false) + }) + + it('should return false when given a TLD that ends with a dash', function () { + assert.strictEqual(util.isValidDomainName('foo.bar-'), false) + }) + + it('should return false when given a domain name with a chunk that starts with a dash', function () { + assert.strictEqual(util.isValidDomainName('-foo.bar'), false) + }) + + it('should return false when given a domain name with a chunk that ends with a dash', function () { + assert.strictEqual(util.isValidDomainName('foo-.bar'), false) + }) + + it('should return false when given a bare TLD', function () { + assert.strictEqual(util.isValidDomainName('bar'), false) + }) + + it('should return false when given a domain that starts with a period', function () { + assert.strictEqual(util.isValidDomainName('.bar'), false) + }) + + it('should return false when given a subdomain that starts with a period', function () { + assert.strictEqual(util.isValidDomainName('.foo.bar'), false) + }) + + it('should return false when given a domain that ends with a period', function () { + assert.strictEqual(util.isValidDomainName('bar.'), false) + }) + + it('should return false when given a 1-character TLD', function () { + assert.strictEqual(util.isValidDomainName('foo.b'), false) + }) + }) + describe('#numericBalance', function () { it('should return a BN 0 if given nothing', function () { var result = util.numericBalance() diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js index 19aabb77084c..90b6fc452c13 100644 --- a/ui/app/helpers/utils/util.js +++ b/ui/app/helpers/utils/util.js @@ -2,6 +2,7 @@ const abi = require('human-standard-token-abi') const ethUtil = require('ethereumjs-util') const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') import { DateTime } from 'luxon' +import punycode from 'punycode' const MIN_GAS_PRICE_GWEI_BN = new ethUtil.BN(1) const GWEI_FACTOR = new ethUtil.BN(1e9) @@ -36,7 +37,7 @@ module.exports = { miniAddressSummary: miniAddressSummary, isAllOneCase: isAllOneCase, isValidAddress: isValidAddress, - isValidENSAddress, + isValidDomainName, numericBalance: numericBalance, parseBalance: parseBalance, formatBalance: formatBalance, @@ -99,8 +100,14 @@ function isValidAddress (address) { return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) } -function isValidENSAddress (address) { - return address.match(/^.{3,}\.(eth|test|xyz)$/) +function isValidDomainName (address) { + const match = punycode.toASCII(address) + .toLowerCase() + // Checks that the domain consists of at least one valid domain pieces separated by periods, followed by a tld + // Each piece of domain name has only the characters a-z, 0-9, and a hyphen (but not at the start or end of chunk) + // A chunk has minimum length of 1, but minimum tld is set to 2 for now (no 1-character tlds exist yet) + .match(/^(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)+[a-z0-9][-a-z0-9]*[a-z0-9]$/) + return match !== null } function isInvalidChecksumAddress (address) { diff --git a/ui/app/pages/send/send-content/add-recipient/ens-input.component.js b/ui/app/pages/send/send-content/add-recipient/ens-input.component.js index 8f8023fd05dd..11b9c5cb5beb 100644 --- a/ui/app/pages/send/send-content/add-recipient/ens-input.component.js +++ b/ui/app/pages/send/send-content/add-recipient/ens-input.component.js @@ -1,8 +1,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import c from 'classnames' -import { isValidENSAddress, isValidAddress, isValidAddressHead } from '../../../../helpers/utils/util' -import {ellipsify} from '../../send.utils' +import { isValidDomainName, isValidAddress, isValidAddressHead } from '../../../../helpers/utils/util' +import { ellipsify } from '../../send.utils' import debounce from 'debounce' import copyToClipboard from 'copy-to-clipboard/index' @@ -91,7 +91,7 @@ export default class EnsInput extends Component { this.props.updateEnsResolution(address) }) .catch((reason) => { - if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') { + if (isValidDomainName(recipient) && reason.message === 'ENS name not defined.') { this.props.updateEnsResolutionError(this.context.t('ensNotFoundOnCurrentNetwork')) } else { log.error(reason) @@ -124,7 +124,7 @@ export default class EnsInput extends Component { return } - if (isValidENSAddress(input)) { + if (isValidDomainName(input)) { this.lookupEnsName(input) } else if (onValidAddressTyped && isValidAddress(input)) { onValidAddressTyped(input) diff --git a/ui/app/pages/send/send.container.js b/ui/app/pages/send/send.container.js index 2350712f3e5f..f8512a7bb1b1 100644 --- a/ui/app/pages/send/send.container.js +++ b/ui/app/pages/send/send.container.js @@ -55,7 +55,7 @@ import { calcGasTotal, } from './send.utils.js' import { - isValidENSAddress, + isValidDomainName, } from '../../helpers/utils/util' import { @@ -126,8 +126,8 @@ function mapDispatchToProps (dispatch) { updateSendEnsResolution: (ensResolution) => dispatch(updateSendEnsResolution(ensResolution)), updateSendEnsResolutionError: (message) => dispatch(updateSendEnsResolutionError(message)), updateToNicknameIfNecessary: (to, toNickname, addressBook) => { - if (isValidENSAddress(toNickname)) { - const addressBookEntry = addressBook.find(({ address}) => to === address) || {} + if (isValidDomainName(toNickname)) { + const addressBookEntry = addressBook.find(({ address }) => to === address) || {} if (!addressBookEntry.name !== toNickname) { dispatch(updateSendTo(to, addressBookEntry.name || '')) } diff --git a/ui/app/pages/settings/contact-list-tab/add-contact/add-contact.component.js b/ui/app/pages/settings/contact-list-tab/add-contact/add-contact.component.js index 56d4415cf5fb..878ea10c2689 100644 --- a/ui/app/pages/settings/contact-list-tab/add-contact/add-contact.component.js +++ b/ui/app/pages/settings/contact-list-tab/add-contact/add-contact.component.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types' import Identicon from '../../../../components/ui/identicon' import TextField from '../../../../components/ui/text-field' import { CONTACT_LIST_ROUTE } from '../../../../helpers/constants/routes' -import { isValidAddress, isValidENSAddress } from '../../../../helpers/utils/util' -import EnsInput from '../../../../pages/send/send-content/add-recipient/ens-input' +import { isValidAddress, isValidDomainName } from '../../../../helpers/utils/util' +import EnsInput from '../../../send/send-content/add-recipient/ens-input' import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer' import debounce from 'lodash.debounce' @@ -51,7 +51,7 @@ export default class AddContact extends PureComponent { validate = address => { const valid = isValidAddress(address) - const validEnsAddress = isValidENSAddress(address) + const validEnsAddress = isValidDomainName(address) if (valid || validEnsAddress || address === '') { this.setState({ error: '', ethAddress: address }) } else {