Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENS reverse resolution support #7177

Merged
merged 9 commits into from
Nov 1, 2019
Merged
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
Copy link
Member

@Gudahtt Gudahtt Oct 31, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it would be nice to know what happened in these cases though - e.g. whether the address didn't resolve, or whether it did but it couldn't be verified, etc.

Maybe a log.debug statement would be appropriate in each of these cases? With something like the error messages you had before.

}

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