aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Finlay <flyswatter@users.noreply.github.com>2017-03-17 01:21:21 +0800
committerGitHub <noreply@github.com>2017-03-17 01:21:21 +0800
commit00f1a2d78d01301f51157e9c8b8352dc0796d034 (patch)
tree91d9cbb8baa26efa462a86e9ecda460792f90996
parent570cc891b5386dc04462737de4df6f2adf5059c9 (diff)
parenta186e40d179d230fc1e81f7f507a5232d23ad462 (diff)
downloadtangerine-wallet-browser-00f1a2d78d01301f51157e9c8b8352dc0796d034.tar.gz
tangerine-wallet-browser-00f1a2d78d01301f51157e9c8b8352dc0796d034.tar.zst
tangerine-wallet-browser-00f1a2d78d01301f51157e9c8b8352dc0796d034.zip
Merge pull request #1198 from MetaMask/i1165-predictive
Add predictive address functionality to recipient field in send
-rw-r--r--CHANGELOG.md1
-rw-r--r--app/scripts/controllers/address-book.js79
-rw-r--r--app/scripts/controllers/preferences.js4
-rw-r--r--app/scripts/metamask-controller.js15
-rw-r--r--test/unit/address-book-controller.js56
-rw-r--r--test/unit/currency-controller-test.js2
-rw-r--r--ui/app/actions.js15
-rw-r--r--ui/app/components/ens-input.js29
-rw-r--r--ui/app/reducers/metamask.js1
-rw-r--r--ui/app/send.js15
10 files changed, 211 insertions, 6 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8e73c286..77dcc53c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
## Current Master
- 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.
## 3.4.0 2017-3-8
diff --git a/app/scripts/controllers/address-book.js b/app/scripts/controllers/address-book.js
new file mode 100644
index 000000000..c66eb2bd4
--- /dev/null
+++ b/app/scripts/controllers/address-book.js
@@ -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
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index 18fccf11b..c7f675a41 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -5,7 +5,9 @@ const extend = require('xtend')
class PreferencesController {
constructor (opts = {}) {
- const initState = extend({ frequentRpcList: [] }, opts.initState)
+ const initState = extend({
+ frequentRpcList: [],
+ }, opts.initState)
this.store = new ObservableStore(initState)
}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 2eaa53200..25f9d9e5d 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -15,6 +15,7 @@ const PreferencesController = require('./controllers/preferences')
const CurrencyController = require('./controllers/currency')
const NoticeController = require('./notice-controller')
const ShapeShiftController = require('./controllers/shapeshift')
+const AddressBookController = require('./controllers/address-book')
const MessageManager = require('./lib/message-manager')
const PersonalMessageManager = require('./lib/personal-message-manager')
const TxManager = require('./transaction-manager')
@@ -80,6 +81,11 @@ module.exports = class MetamaskController extends EventEmitter {
autoFaucet(address)
})
+ // address book controller
+ this.addressBookController = new AddressBookController({
+ initState: initState.AddressBookController,
+ }, this.keyringController)
+
// tx mgmt
this.txManager = new TxManager({
initState: initState.TransactionManager,
@@ -124,6 +130,9 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController.store.subscribe((state) => {
this.store.updateState({ PreferencesController: state })
})
+ this.addressBookController.store.subscribe((state) => {
+ this.store.updateState({ AddressBookController: state })
+ })
this.currencyController.store.subscribe((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.keyringController.memStore.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.noticeController.memStore.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.keyringController.memStore.getState(),
this.preferencesController.store.getState(),
+ this.addressBookController.store.getState(),
this.currencyController.store.getState(),
this.noticeController.memStore.getState(),
// config manager
@@ -240,6 +251,7 @@ module.exports = class MetamaskController extends EventEmitter {
const preferencesController = this.preferencesController
const txManager = this.txManager
const noticeController = this.noticeController
+ const addressBookController = this.addressBookController
return {
// etc
@@ -267,6 +279,9 @@ module.exports = class MetamaskController extends EventEmitter {
setDefaultRpc: nodeify(this.setDefaultRpc).bind(this),
setCustomRpc: nodeify(this.setCustomRpc).bind(this),
+ // AddressController
+ setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController),
+
// KeyringController
setLocked: nodeify(keyringController.setLocked).bind(keyringController),
createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController),
diff --git a/test/unit/address-book-controller.js b/test/unit/address-book-controller.js
new file mode 100644
index 000000000..f345b0328
--- /dev/null
+++ b/test/unit/address-book-controller.js
@@ -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.')
+ })
+ })
+ })
+})
diff --git a/test/unit/currency-controller-test.js b/test/unit/currency-controller-test.js
index dd7fa91e0..079f8b488 100644
--- a/test/unit/currency-controller-test.js
+++ b/test/unit/currency-controller-test.js
@@ -7,7 +7,7 @@ const rp = require('request-promise')
const nock = require('nock')
const CurrencyController = require('../../app/scripts/controllers/currency')
-describe('config-manager', function() {
+describe('currency-controller', function() {
var currencyController
beforeEach(function() {
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 0027a5f67..b09021577 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -75,6 +75,8 @@ var actions = {
// account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
showSendPage: showSendPage,
+ ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK',
+ addToAddressBook: addToAddressBook,
REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT',
requestExportAccount: requestExportAccount,
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) {
log.debug(`background.setProviderType`)
background.setProviderType(type)
diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js
index ffc4eab4a..facf29d97 100644
--- a/ui/app/components/ens-input.js
+++ b/ui/app/components/ens-input.js
@@ -21,6 +21,7 @@ function EnsInput () {
EnsInput.prototype.render = function () {
const props = this.props
const opts = extend(props, {
+ list: 'addresses',
onChange: () => {
const network = this.props.network
let resolverAddress = networkResolvers[network]
@@ -46,6 +47,25 @@ EnsInput.prototype.render = function () {
style: { width: '100%' },
}, [
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(),
])
}
@@ -80,11 +100,13 @@ EnsInput.prototype.lookupEnsName = function () {
this.setState({
loadingEns: false,
ensResolution: address,
+ nickname: recipient.trim(),
hoverText: address + '\nClick to Copy',
})
}
})
.catch((reason) => {
+ log.error(reason)
return this.setState({
loadingEns: false,
ensFailure: true,
@@ -95,10 +117,13 @@ EnsInput.prototype.lookupEnsName = function () {
EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
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 &&
ensResolution !== prevState.ensResolution) {
- this.props.onChange(ensResolution)
+ this.props.onChange(ensResolution, nickname)
}
}
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
index 269f8d272..2b5151466 100644
--- a/ui/app/reducers/metamask.js
+++ b/ui/app/reducers/metamask.js
@@ -16,6 +16,7 @@ function reduceMetamask (state, action) {
noActiveNotices: true,
lastUnreadNotice: undefined,
frequentRpcList: [],
+ addressBook: [],
}, state.metamask)
switch (action.type) {
diff --git a/ui/app/send.js b/ui/app/send.js
index a281a5fcf..eb32d5e06 100644
--- a/ui/app/send.js
+++ b/ui/app/send.js
@@ -20,6 +20,7 @@ function mapStateToProps (state) {
identities: state.metamask.identities,
warning: state.appState.warning,
network: state.metamask.network,
+ addressBook: state.metamask.addressBook,
}
result.error = result.warning && result.warning.split('.')[0]
@@ -44,6 +45,8 @@ SendTransactionScreen.prototype.render = function () {
var account = state.account
var identity = state.identity
var network = state.network
+ var identities = state.identities
+ var addressBook = state.addressBook
return (
@@ -153,6 +156,8 @@ SendTransactionScreen.prototype.render = function () {
placeholder: 'Recipient Address',
onChange: this.recipientDidChange.bind(this),
network,
+ identities,
+ addressBook,
}),
]),
@@ -222,13 +227,17 @@ SendTransactionScreen.prototype.back = function () {
this.props.dispatch(actions.backToAccountDetail(address))
}
-SendTransactionScreen.prototype.recipientDidChange = function (recipient) {
- this.setState({ recipient })
+SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) {
+ this.setState({
+ recipient: recipient,
+ nickname: nickname,
+ })
}
SendTransactionScreen.prototype.onSubmit = function () {
const state = this.state || {}
const recipient = state.recipient || document.querySelector('input[name="address"]').value
+ const nickname = state.nickname || ' '
const input = document.querySelector('input[name="amount"]').value
const value = util.normalizeEthStringToWei(input)
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.addToAddressBook(recipient, nickname))
+
var txParams = {
from: this.props.address,
value: '0x' + value.toString(16),