diff --git a/app/scripts/controllers/ens/ens.js b/app/scripts/controllers/ens/ens.js new file mode 100644 index 000000000..eb2586a7d --- /dev/null +++ b/app/scripts/controllers/ens/ens.js @@ -0,0 +1,25 @@ +const EthJsEns = require('ethjs-ens') +const ensNetworkMap = require('ethjs-ens/lib/network-map.json') + +class Ens { + static getNetworkEnsSupport (network) { + return Boolean(ensNetworkMap[network]) + } + + constructor ({ network, provider } = {}) { + this._ethJsEns = new EthJsEns({ + network, + provider, + }) + } + + lookup (ensName) { + return this._ethJsEns.lookup(ensName) + } + + reverse (address) { + return this._ethJsEns.reverse(address) + } +} + +module.exports = Ens diff --git a/app/scripts/controllers/ens/index.js b/app/scripts/controllers/ens/index.js new file mode 100644 index 000000000..6456f8b53 --- /dev/null +++ b/app/scripts/controllers/ens/index.js @@ -0,0 +1,75 @@ +const ethUtil = require('ethereumjs-util') +const ObservableStore = require('obs-store') +const punycode = require('punycode') +const Ens = require('./ens') + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const ZERO_X_ERROR_ADDRESS = '0x' + +class EnsController { + constructor ({ ens, provider, networkStore } = {}) { + const initState = { + ensResolutionsByAddress: {}, + } + + this._ens = ens + if (!this._ens) { + const network = networkStore.getState() + if (Ens.getNetworkEnsSupport(network)) { + this._ens = new Ens({ + network, + provider, + }) + } + } + + this.store = new ObservableStore(initState) + networkStore.subscribe((network) => { + this.store.putState(initState) + this._ens = new Ens({ + network, + provider, + }) + }) + } + + reverseResolveAddress (address) { + return this._reverseResolveAddress(ethUtil.toChecksumAddress(address)) + } + + async _reverseResolveAddress (address) { + if (!this._ens) { + return undefined + } + + const state = this.store.getState() + if (state.ensResolutionsByAddress[address]) { + return state.ensResolutionsByAddress[address] + } + + const domain = await this._ens.reverse(address) + const registeredAddress = await this._ens.lookup(domain) + if (registeredAddress === ZERO_ADDRESS || registeredAddress === ZERO_X_ERROR_ADDRESS) { + return undefined + } + + if (ethUtil.toChecksumAddress(registeredAddress) !== address) { + return undefined + } + + this._updateResolutionsByAddress(address, punycode.toASCII(domain)) + return domain + } + + _updateResolutionsByAddress (address, domain) { + const oldState = this.store.getState() + this.store.putState({ + ensResolutionsByAddress: { + ...oldState.ensResolutionsByAddress, + [address]: domain, + }, + }) + } +} + +module.exports = EnsController diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index eac6d1e81..1c607a4c6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -23,6 +23,7 @@ const createLoggerMiddleware = require('./lib/createLoggerMiddleware') const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware') const {setupMultiplex} = require('./lib/stream-utils.js') const KeyringController = require('eth-keyring-controller') +const EnsController = require('./controllers/ens') const NetworkController = require('./controllers/network') const PreferencesController = require('./controllers/preferences') const AppStateController = require('./controllers/app-state') @@ -138,6 +139,11 @@ module.exports = class MetamaskController extends EventEmitter { networkController: this.networkController, }) + this.ensController = new EnsController({ + provider: this.provider, + networkStore: this.networkController.networkStore, + }) + this.incomingTransactionsController = new IncomingTransactionsController({ blockTracker: this.blockTracker, networkController: this.networkController, @@ -315,6 +321,8 @@ module.exports = class MetamaskController extends EventEmitter { // ThreeBoxController ThreeBoxController: this.threeBoxController.store, ABTestController: this.abTestController.store, + // ENS Controller + EnsController: this.ensController.store, }) this.memStore.subscribe(this.sendUpdate.bind(this)) } @@ -501,6 +509,9 @@ module.exports = class MetamaskController extends EventEmitter { // AppStateController setLastActiveTime: nodeify(this.appStateController.setLastActiveTime, this.appStateController), + // EnsController + tryReverseResolveAddress: nodeify(this.ensController.reverseResolveAddress, this.ensController), + // KeyringController setLocked: nodeify(this.setLocked, this), createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this), diff --git a/package.json b/package.json index 3a8d5ea1a..5c155f5ea 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,7 @@ "prop-types": "^15.6.1", "pubnub": "4.24.4", "pump": "^3.0.0", + "punycode": "^2.1.1", "qrcode-generator": "1.4.1", "ramda": "^0.24.1", "react": "^15.6.2", diff --git a/test/unit/app/controllers/ens-controller-test.js b/test/unit/app/controllers/ens-controller-test.js new file mode 100644 index 000000000..1eb52a17c --- /dev/null +++ b/test/unit/app/controllers/ens-controller-test.js @@ -0,0 +1,135 @@ +const assert = require('assert') +const sinon = require('sinon') +const ObservableStore = require('obs-store') +const HttpProvider = require('ethjs-provider-http') +const EnsController = require('../../../../app/scripts/controllers/ens') + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const ZERO_X_ERROR_ADDRESS = '0x' + +describe('EnsController', function () { + describe('#constructor', function () { + it('should construct the controller given a provider and a network', async () => { + const provider = new HttpProvider('https://ropsten.infura.io') + const currentNetworkId = '3' + const networkStore = new ObservableStore(currentNetworkId) + const ens = new EnsController({ + provider, + networkStore, + }) + + assert.ok(ens._ens) + }) + + it('should construct the controller given an existing ENS instance', async () => { + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: {}, + networkStore, + }) + + assert.ok(ens._ens) + }) + }) + + describe('#reverseResolveName', function () { + it('should resolve to an ENS name', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns(address), + }, + networkStore, + }) + + const name = await ens.reverseResolveAddress(address) + assert.equal(name, 'peaksignal.eth') + }) + + it('should only resolve an ENS name once', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const reverse = sinon.stub().withArgs(address).returns('peaksignal.eth') + const lookup = sinon.stub().withArgs('peaksignal.eth').returns(address) + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse, + lookup, + }, + networkStore, + }) + + assert.equal(await ens.reverseResolveAddress(address), 'peaksignal.eth') + assert.equal(await ens.reverseResolveAddress(address), 'peaksignal.eth') + assert.ok(lookup.calledOnce) + assert.ok(reverse.calledOnce) + }) + + it('should fail if the name is registered to a different address than the reverse-resolved', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns('0xfoo'), + }, + networkStore, + }) + + const name = await ens.reverseResolveAddress(address) + assert.strictEqual(name, undefined) + }) + + it('should throw an error when the lookup resolves to the zero address', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns(ZERO_ADDRESS), + }, + networkStore, + }) + + try { + await ens.reverseResolveAddress(address) + assert.fail('#reverseResolveAddress did not throw') + } catch (e) { + assert.ok(e) + } + }) + + it('should throw an error the lookup resolves to the zero x address', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns(ZERO_X_ERROR_ADDRESS), + }, + networkStore, + }) + + try { + await ens.reverseResolveAddress(address) + assert.fail('#reverseResolveAddress did not throw') + } catch (e) { + assert.ok(e) + } + }) + }) +}) diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js index d26daf786..41d9d5952 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js @@ -24,6 +24,7 @@ export default class ConfirmPageContainer extends Component { fromName: PropTypes.string, toAddress: PropTypes.string, toName: PropTypes.string, + toEns: PropTypes.string, toNickname: PropTypes.string, // Content contentComponent: PropTypes.node, @@ -69,6 +70,7 @@ export default class ConfirmPageContainer extends Component { fromName, fromAddress, toName, + toEns, toNickname, toAddress, disabled, @@ -128,6 +130,7 @@ export default class ConfirmPageContainer extends Component { senderAddress={fromAddress} recipientName={toName} recipientAddress={toAddress} + recipientEns={toEns} recipientNickname={toNickname} assetImage={renderAssetImage ? assetImage : undefined} /> diff --git a/ui/app/components/app/transaction-list-item-details/index.js b/ui/app/components/app/transaction-list-item-details/index.js index 0e878d032..83bd53e7d 100644 --- a/ui/app/components/app/transaction-list-item-details/index.js +++ b/ui/app/components/app/transaction-list-item-details/index.js @@ -1 +1 @@ -export { default } from './transaction-list-item-details.component' +export { default } from './transaction-list-item-details.container' diff --git a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 983bbf6e5..f27c74970 100644 --- a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -17,6 +17,10 @@ export default class TransactionListItemDetails extends PureComponent { metricsEvent: PropTypes.func, } + static defaultProps = { + recipientEns: null, + } + static propTypes = { onCancel: PropTypes.func, onRetry: PropTypes.func, @@ -26,7 +30,11 @@ export default class TransactionListItemDetails extends PureComponent { isEarliestNonce: PropTypes.bool, cancelDisabled: PropTypes.bool, transactionGroup: PropTypes.object, + recipientEns: PropTypes.string, + recipientAddress: PropTypes.string.isRequired, rpcPrefs: PropTypes.object, + senderAddress: PropTypes.string.isRequired, + tryReverseResolveAddress: PropTypes.func.isRequired, } state = { @@ -82,6 +90,12 @@ export default class TransactionListItemDetails extends PureComponent { }) } + async componentDidMount () { + const { recipientAddress, tryReverseResolveAddress } = this.props + + tryReverseResolveAddress(recipientAddress) + } + renderCancel () { const { t } = this.context const { @@ -128,11 +142,14 @@ export default class TransactionListItemDetails extends PureComponent { showRetry, onCancel, onRetry, + recipientEns, + recipientAddress, rpcPrefs: { blockExplorerUrl } = {}, + senderAddress, isEarliestNonce, } = this.props const { primaryTransaction: transaction } = transactionGroup - const { hash, txParams: { to, from } = {} } = transaction + const { hash } = transaction return (
{t('copyAddress')}
+ : ( +
+ {addressSlicer(checksummedSenderAddress)}
+ {t('copyAddress')}
+
{t('copyAddress')}
+ : ( +
+ {addressSlicer(checksummedRecipientAddress)}
+ {t('copyAddress')}
+