Merge pull request #1198 from MetaMask/i1165-predictive

Add predictive address functionality to recipient field in send
feature/default_network_editable
Dan Finlay 8 years ago committed by GitHub
commit 00f1a2d78d
  1. 1
      CHANGELOG.md
  2. 79
      app/scripts/controllers/address-book.js
  3. 4
      app/scripts/controllers/preferences.js
  4. 15
      app/scripts/metamask-controller.js
  5. 56
      test/unit/address-book-controller.js
  6. 2
      test/unit/currency-controller-test.js
  7. 15
      ui/app/actions.js
  8. 29
      ui/app/components/ens-input.js
  9. 1
      ui/app/reducers/metamask.js
  10. 15
      ui/app/send.js

@ -3,6 +3,7 @@
## Current Master ## Current Master
- Allow sending to ENS names in send form on Ropsten. - Allow sending to ENS names in send form on Ropsten.
- Added an address book functionality that remembers the last 15 unique addresses sent to.
- Can now change network to custom RPC URL from lock screen. - Can now change network to custom RPC URL from lock screen.
## 3.4.0 2017-3-8 ## 3.4.0 2017-3-8

@ -0,0 +1,79 @@
const ObservableStore = require('obs-store')
const extend = require('xtend')
class AddressBookController {
// Controller in charge of managing the address book functionality from the
// recipients field on the send screen. Manages a history of all saved
// addresses and all currently owned addresses.
constructor (opts = {}, keyringController) {
const initState = extend({
addressBook: [],
}, opts.initState)
this.store = new ObservableStore(initState)
this.keyringController = keyringController
}
//
// PUBLIC METHODS
//
// Sets a new address book in store by accepting a new address and nickname.
setAddressBook (address, name) {
return this._addToAddressBook(address, name)
.then((addressBook) => {
this.store.updateState({
addressBook,
})
return Promise.resolve()
})
}
//
// PRIVATE METHODS
//
// Performs the logic to add the address and name into the address book. The
// pushed object is an object of two fields. Current behavior does not set an
// upper limit to the number of addresses.
_addToAddressBook (address, name) {
let addressBook = this._getAddressBook()
let identities = this._getIdentities()
let addressBookIndex = addressBook.findIndex((element) => { return element.address.toLowerCase() === address.toLowerCase() || element.name === name })
let identitiesIndex = Object.keys(identities).findIndex((element) => { return element.toLowerCase() === address.toLowerCase() })
// trigger this condition if we own this address--no need to overwrite.
if (identitiesIndex !== -1) {
return Promise.resolve(addressBook)
// trigger this condition if we've seen this address before--may need to update nickname.
} else if (addressBookIndex !== -1) {
addressBook.splice(addressBookIndex, 1)
} else if (addressBook.length > 15) {
addressBook.shift()
}
addressBook.push({
address: address,
name,
})
return Promise.resolve(addressBook)
}
// Internal method to get the address book. Current persistence behavior
// should not require that this method be called from the UI directly.
_getAddressBook () {
return this.store.getState().addressBook
}
// Retrieves identities from the keyring controller in order to avoid
// duplication
_getIdentities () {
return this.keyringController.memStore.getState().identities
}
}
module.exports = AddressBookController

@ -5,7 +5,9 @@ const extend = require('xtend')
class PreferencesController { class PreferencesController {
constructor (opts = {}) { constructor (opts = {}) {
const initState = extend({ frequentRpcList: [] }, opts.initState) const initState = extend({
frequentRpcList: [],
}, opts.initState)
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
} }

@ -15,6 +15,7 @@ const PreferencesController = require('./controllers/preferences')
const CurrencyController = require('./controllers/currency') const CurrencyController = require('./controllers/currency')
const NoticeController = require('./notice-controller') const NoticeController = require('./notice-controller')
const ShapeShiftController = require('./controllers/shapeshift') const ShapeShiftController = require('./controllers/shapeshift')
const AddressBookController = require('./controllers/address-book')
const MessageManager = require('./lib/message-manager') const MessageManager = require('./lib/message-manager')
const PersonalMessageManager = require('./lib/personal-message-manager') const PersonalMessageManager = require('./lib/personal-message-manager')
const TxManager = require('./transaction-manager') const TxManager = require('./transaction-manager')
@ -80,6 +81,11 @@ module.exports = class MetamaskController extends EventEmitter {
autoFaucet(address) autoFaucet(address)
}) })
// address book controller
this.addressBookController = new AddressBookController({
initState: initState.AddressBookController,
}, this.keyringController)
// tx mgmt // tx mgmt
this.txManager = new TxManager({ this.txManager = new TxManager({
initState: initState.TransactionManager, initState: initState.TransactionManager,
@ -124,6 +130,9 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController.store.subscribe((state) => { this.preferencesController.store.subscribe((state) => {
this.store.updateState({ PreferencesController: state }) this.store.updateState({ PreferencesController: state })
}) })
this.addressBookController.store.subscribe((state) => {
this.store.updateState({ AddressBookController: state })
})
this.currencyController.store.subscribe((state) => { this.currencyController.store.subscribe((state) => {
this.store.updateState({ CurrencyController: state }) this.store.updateState({ CurrencyController: state })
}) })
@ -142,6 +151,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this))
this.keyringController.memStore.subscribe(this.sendUpdate.bind(this)) this.keyringController.memStore.subscribe(this.sendUpdate.bind(this))
this.preferencesController.store.subscribe(this.sendUpdate.bind(this)) this.preferencesController.store.subscribe(this.sendUpdate.bind(this))
this.addressBookController.store.subscribe(this.sendUpdate.bind(this))
this.currencyController.store.subscribe(this.sendUpdate.bind(this)) this.currencyController.store.subscribe(this.sendUpdate.bind(this))
this.noticeController.memStore.subscribe(this.sendUpdate.bind(this)) this.noticeController.memStore.subscribe(this.sendUpdate.bind(this))
this.shapeshiftController.store.subscribe(this.sendUpdate.bind(this)) this.shapeshiftController.store.subscribe(this.sendUpdate.bind(this))
@ -219,6 +229,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.personalMessageManager.memStore.getState(), this.personalMessageManager.memStore.getState(),
this.keyringController.memStore.getState(), this.keyringController.memStore.getState(),
this.preferencesController.store.getState(), this.preferencesController.store.getState(),
this.addressBookController.store.getState(),
this.currencyController.store.getState(), this.currencyController.store.getState(),
this.noticeController.memStore.getState(), this.noticeController.memStore.getState(),
// config manager // config manager
@ -240,6 +251,7 @@ module.exports = class MetamaskController extends EventEmitter {
const preferencesController = this.preferencesController const preferencesController = this.preferencesController
const txManager = this.txManager const txManager = this.txManager
const noticeController = this.noticeController const noticeController = this.noticeController
const addressBookController = this.addressBookController
return { return {
// etc // etc
@ -267,6 +279,9 @@ module.exports = class MetamaskController extends EventEmitter {
setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), setDefaultRpc: nodeify(this.setDefaultRpc).bind(this),
setCustomRpc: nodeify(this.setCustomRpc).bind(this), setCustomRpc: nodeify(this.setCustomRpc).bind(this),
// AddressController
setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController),
// KeyringController // KeyringController
setLocked: nodeify(keyringController.setLocked).bind(keyringController), setLocked: nodeify(keyringController.setLocked).bind(keyringController),
createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController), createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController),

@ -0,0 +1,56 @@
const assert = require('assert')
const extend = require('xtend')
const AddressBookController = require('../../app/scripts/controllers/address-book')
const mockKeyringController = {
memStore: {
getState: function () {
return {
identities: {
'0x0aaa' : {
address: '0x0aaa',
name: 'owned',
}
}
}
}
}
}
describe('address-book-controller', function() {
var addressBookController
beforeEach(function() {
addressBookController = new AddressBookController({}, mockKeyringController)
})
describe('addres book management', function () {
describe('#_getAddressBook', function () {
it('should be empty by default.', function () {
assert.equal(addressBookController._getAddressBook().length, 0)
})
})
describe('#setAddressBook', function () {
it('should properly set a new address.', function () {
addressBookController.setAddressBook('0x01234', 'test')
var addressBook = addressBookController._getAddressBook()
assert.equal(addressBook.length, 1, 'incorrect address book length.')
assert.equal(addressBook[0].address, '0x01234', 'incorrect addresss')
assert.equal(addressBook[0].name, 'test', 'incorrect nickname')
})
it('should reject duplicates.', function () {
addressBookController.setAddressBook('0x01234', 'test')
addressBookController.setAddressBook('0x01234', 'test')
var addressBook = addressBookController._getAddressBook()
assert.equal(addressBook.length, 1, 'incorrect address book length.')
})
it('should not add any identities that are under user control', function () {
addressBookController.setAddressBook('0x0aaa', ' ')
var addressBook = addressBookController._getAddressBook()
assert.equal(addressBook.length, 0, 'incorrect address book length.')
})
})
})
})

@ -7,7 +7,7 @@ const rp = require('request-promise')
const nock = require('nock') const nock = require('nock')
const CurrencyController = require('../../app/scripts/controllers/currency') const CurrencyController = require('../../app/scripts/controllers/currency')
describe('config-manager', function() { describe('currency-controller', function() {
var currencyController var currencyController
beforeEach(function() { beforeEach(function() {

@ -75,6 +75,8 @@ var actions = {
// account detail screen // account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
showSendPage: showSendPage, showSendPage: showSendPage,
ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK',
addToAddressBook: addToAddressBook,
REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT',
requestExportAccount: requestExportAccount, requestExportAccount: requestExportAccount,
EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', EXPORT_ACCOUNT: 'EXPORT_ACCOUNT',
@ -696,6 +698,19 @@ function setRpcTarget (newRpc) {
} }
} }
// Calls the addressBookController to add a new address.
function addToAddressBook (recipient, nickname) {
log.debug(`background.addToAddressBook`)
return (dispatch) => {
background.setAddressBook(recipient, nickname, (err, result) => {
if (err) {
log.error(err)
return dispatch(self.displayWarning('Address book failed to update'))
}
})
}
}
function setProviderType (type) { function setProviderType (type) {
log.debug(`background.setProviderType`) log.debug(`background.setProviderType`)
background.setProviderType(type) background.setProviderType(type)

@ -21,6 +21,7 @@ function EnsInput () {
EnsInput.prototype.render = function () { EnsInput.prototype.render = function () {
const props = this.props const props = this.props
const opts = extend(props, { const opts = extend(props, {
list: 'addresses',
onChange: () => { onChange: () => {
const network = this.props.network const network = this.props.network
let resolverAddress = networkResolvers[network] let resolverAddress = networkResolvers[network]
@ -46,6 +47,25 @@ EnsInput.prototype.render = function () {
style: { width: '100%' }, style: { width: '100%' },
}, [ }, [
h('input.large-input', opts), h('input.large-input', opts),
// The address book functionality.
h('datalist#addresses',
[
// Corresponds to the addresses owned.
Object.keys(props.identities).map((key) => {
let identity = props.identities[key]
return h('option', {
value: identity.address,
label: identity.name,
})
}),
// Corresponds to previously sent-to addresses.
props.addressBook.map((identity) => {
return h('option', {
value: identity.address,
label: identity.name,
})
}),
]),
this.ensIcon(), this.ensIcon(),
]) ])
} }
@ -80,11 +100,13 @@ EnsInput.prototype.lookupEnsName = function () {
this.setState({ this.setState({
loadingEns: false, loadingEns: false,
ensResolution: address, ensResolution: address,
nickname: recipient.trim(),
hoverText: address + '\nClick to Copy', hoverText: address + '\nClick to Copy',
}) })
} }
}) })
.catch((reason) => { .catch((reason) => {
log.error(reason)
return this.setState({ return this.setState({
loadingEns: false, loadingEns: false,
ensFailure: true, ensFailure: true,
@ -95,10 +117,13 @@ EnsInput.prototype.lookupEnsName = function () {
EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
const state = this.state || {} const state = this.state || {}
const { ensResolution } = state const ensResolution = state.ensResolution
// If an address is sent without a nickname, meaning not from ENS or from
// the user's own accounts, a default of a one-space string is used.
const nickname = state.nickname || ' '
if (ensResolution && this.props.onChange && if (ensResolution && this.props.onChange &&
ensResolution !== prevState.ensResolution) { ensResolution !== prevState.ensResolution) {
this.props.onChange(ensResolution) this.props.onChange(ensResolution, nickname)
} }
} }

@ -16,6 +16,7 @@ function reduceMetamask (state, action) {
noActiveNotices: true, noActiveNotices: true,
lastUnreadNotice: undefined, lastUnreadNotice: undefined,
frequentRpcList: [], frequentRpcList: [],
addressBook: [],
}, state.metamask) }, state.metamask)
switch (action.type) { switch (action.type) {

@ -20,6 +20,7 @@ function mapStateToProps (state) {
identities: state.metamask.identities, identities: state.metamask.identities,
warning: state.appState.warning, warning: state.appState.warning,
network: state.metamask.network, network: state.metamask.network,
addressBook: state.metamask.addressBook,
} }
result.error = result.warning && result.warning.split('.')[0] result.error = result.warning && result.warning.split('.')[0]
@ -44,6 +45,8 @@ SendTransactionScreen.prototype.render = function () {
var account = state.account var account = state.account
var identity = state.identity var identity = state.identity
var network = state.network var network = state.network
var identities = state.identities
var addressBook = state.addressBook
return ( return (
@ -153,6 +156,8 @@ SendTransactionScreen.prototype.render = function () {
placeholder: 'Recipient Address', placeholder: 'Recipient Address',
onChange: this.recipientDidChange.bind(this), onChange: this.recipientDidChange.bind(this),
network, network,
identities,
addressBook,
}), }),
]), ]),
@ -222,13 +227,17 @@ SendTransactionScreen.prototype.back = function () {
this.props.dispatch(actions.backToAccountDetail(address)) this.props.dispatch(actions.backToAccountDetail(address))
} }
SendTransactionScreen.prototype.recipientDidChange = function (recipient) { SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) {
this.setState({ recipient }) this.setState({
recipient: recipient,
nickname: nickname,
})
} }
SendTransactionScreen.prototype.onSubmit = function () { SendTransactionScreen.prototype.onSubmit = function () {
const state = this.state || {} const state = this.state || {}
const recipient = state.recipient || document.querySelector('input[name="address"]').value const recipient = state.recipient || document.querySelector('input[name="address"]').value
const nickname = state.nickname || ' '
const input = document.querySelector('input[name="amount"]').value const input = document.querySelector('input[name="amount"]').value
const value = util.normalizeEthStringToWei(input) const value = util.normalizeEthStringToWei(input)
const txData = document.querySelector('input[name="txData"]').value const txData = document.querySelector('input[name="txData"]').value
@ -257,6 +266,8 @@ SendTransactionScreen.prototype.onSubmit = function () {
this.props.dispatch(actions.hideWarning()) this.props.dispatch(actions.hideWarning())
this.props.dispatch(actions.addToAddressBook(recipient, nickname))
var txParams = { var txParams = {
from: this.props.address, from: this.props.address,
value: '0x' + value.toString(16), value: '0x' + value.toString(16),

Loading…
Cancel
Save