Skip to content

Commit

Permalink
ENS Reverse Resolution support (#7177)
Browse files Browse the repository at this point in the history
* ENS Reverse Resolution support
* Save punycode for ENS domains with Unicode characters
* Update SenderToRecipient recipientEns tooltip
* Use cached results when reverse-resolving ENS names
* Display ENS names in tx activity log
  • Loading branch information
whymarrh authored Nov 1, 2019
1 parent f9cd775 commit eed4a9e
Show file tree
Hide file tree
Showing 16 changed files with 373 additions and 17 deletions.
25 changes: 25 additions & 0 deletions app/scripts/controllers/ens/ens.js
Original file line number Diff line number Diff line change
@@ -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
75 changes: 75 additions & 0 deletions app/scripts/controllers/ens/index.js
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
135 changes: 135 additions & 0 deletions test/unit/app/controllers/ens-controller-test.js
Original file line number Diff line number Diff line change
@@ -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)
}
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -69,6 +70,7 @@ export default class ConfirmPageContainer extends Component {
fromName,
fromAddress,
toName,
toEns,
toNickname,
toAddress,
disabled,
Expand Down Expand Up @@ -128,6 +130,7 @@ export default class ConfirmPageContainer extends Component {
senderAddress={fromAddress}
recipientName={toName}
recipientAddress={toAddress}
recipientEns={toEns}
recipientNickname={toNickname}
assetImage={renderAssetImage ? assetImage : undefined}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default } from './transaction-list-item-details.component'
export { default } from './transaction-list-item-details.container'
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 (
<div className="transaction-list-item-details">
Expand Down Expand Up @@ -192,8 +209,9 @@ export default class TransactionListItemDetails extends PureComponent {
<SenderToRecipient
variant={FLAT_VARIANT}
addressOnly
recipientAddress={to}
senderAddress={from}
recipientEns={recipientEns}
recipientAddress={recipientAddress}
senderAddress={senderAddress}
onRecipientClick={() => {
this.context.metricsEvent({
eventOpts: {
Expand Down
Loading

0 comments on commit eed4a9e

Please sign in to comment.