Skip to content

Commit

Permalink
Merge pull request #4884 from MetaMask/TokensPerAccountBasis
Browse files Browse the repository at this point in the history
Tokens per account & network basis
  • Loading branch information
danfinlay authored Aug 7, 2018
2 parents d549c4b + 66c76c0 commit e9f74f0
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 37 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Current Master

- [#4884](https://github.com/MetaMask/metamask-extension/pull/4884): Allow to have tokens per account and network.

## 4.9.0 Tue Aug 07 2018

- Add new tokens auto detection
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/controllers/detect-tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class DetectTokensController {
set preferences (preferences) {
if (!preferences) { return }
this._preferences = preferences
preferences.store.subscribe(({ tokens }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
preferences.store.subscribe(({ tokens = [] }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
preferences.store.subscribe(({ selectedAddress }) => {
if (this.selectedAddress !== selectedAddress) {
this.selectedAddress = selectedAddress
Expand Down
94 changes: 78 additions & 16 deletions app/scripts/controllers/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class PreferencesController {
* @property {array} store.frequentRpcList A list of custom rpcs to provide the user
* @property {string} store.currentAccountTab Indicates the selected tab in the ui
* @property {array} store.tokens The tokens the user wants display in their token lists
* @property {object} store.accountTokens The tokens stored per account and then per network type
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature
Expand All @@ -24,6 +25,7 @@ class PreferencesController {
const initState = extend({
frequentRpcList: [],
currentAccountTab: 'history',
accountTokens: {},
tokens: [],
useBlockie: false,
featureFlags: {},
Expand All @@ -33,8 +35,9 @@ class PreferencesController {
}, opts.initState)

this.diagnostics = opts.diagnostics

this.network = opts.network
this.store = new ObservableStore(initState)
this._subscribeProviderType()
}
// PUBLIC METHODS

Expand Down Expand Up @@ -77,12 +80,19 @@ class PreferencesController {
*/
setAddresses (addresses) {
const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens

const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {}
ids[address] = {name: `Account ${index + 1}`, address, ...oldId}
return ids
}, {})
this.store.updateState({ identities })
const accountTokens = addresses.reduce((tokens, address) => {
const oldTokens = oldAccountTokens[address] || {}
tokens[address] = oldTokens
return tokens
}, {})
this.store.updateState({ identities, accountTokens })
}

/**
Expand All @@ -93,11 +103,13 @@ class PreferencesController {
*/
removeAddress (address) {
const identities = this.store.getState().identities
const accountTokens = this.store.getState().accountTokens
if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`)
}
delete identities[address]
this.store.updateState({ identities })
delete accountTokens[address]
this.store.updateState({ identities, accountTokens })

// If the selected account is no longer valid,
// select an arbitrary other account:
Expand All @@ -117,14 +129,17 @@ class PreferencesController {
*/
addAddresses (addresses) {
const identities = this.store.getState().identities
const accountTokens = this.store.getState().accountTokens
addresses.forEach((address) => {
// skip if already exists
if (identities[address]) return
// add missing identity
const identityCount = Object.keys(identities).length

accountTokens[address] = {}
identities[address] = { name: `Account ${identityCount + 1}`, address }
})
this.store.updateState({ identities })
this.store.updateState({ identities, accountTokens })
}

/*
Expand Down Expand Up @@ -175,15 +190,15 @@ class PreferencesController {
* Setter for the `selectedAddress` property
*
* @param {string} _address A new hex address for an account
* @returns {Promise<void>} Promise resolves with undefined
* @returns {Promise<void>} Promise resolves with tokens
*
*/
setSelectedAddress (_address) {
return new Promise((resolve, reject) => {
const address = normalizeAddress(_address)
this.store.updateState({ selectedAddress: address })
resolve()
})
const address = normalizeAddress(_address)
this._updateTokens(address)
this.store.updateState({ selectedAddress: address })
const tokens = this.store.getState().tokens
return Promise.resolve(tokens)
}

/**
Expand Down Expand Up @@ -232,9 +247,7 @@ class PreferencesController {
} else {
tokens.push(newEntry)
}

this.store.updateState({ tokens })

this._updateAccountTokens(tokens)
return Promise.resolve(tokens)
}

Expand All @@ -247,10 +260,8 @@ class PreferencesController {
*/
removeToken (rawAddress) {
const tokens = this.store.getState().tokens

const updatedTokens = tokens.filter(token => token.address !== rawAddress)

this.store.updateState({ tokens: updatedTokens })
this._updateAccountTokens(updatedTokens)
return Promise.resolve(updatedTokens)
}

Expand Down Expand Up @@ -376,6 +387,57 @@ class PreferencesController {
//
// PRIVATE METHODS
//
/**
* Subscription to network provider type.
*
*
*/
_subscribeProviderType () {
this.network.providerStore.subscribe(() => {
const { tokens } = this._getTokenRelatedStates()
this.store.updateState({ tokens })
})
}

/**
* Updates `accountTokens` and `tokens` of current account and network according to it.
*
* @param {array} tokens Array of tokens to be updated.
*
*/
_updateAccountTokens (tokens) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens })
}

/**
* Updates `tokens` of current account and network.
*
* @param {string} selectedAddress Account address to be updated with.
*
*/
_updateTokens (selectedAddress) {
const { tokens } = this._getTokenRelatedStates(selectedAddress)
this.store.updateState({ tokens })
}

/**
* A getter for `tokens` and `accountTokens` related states.
*
* @param {string} selectedAddress A new hex address for an account
* @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
*
*/
_getTokenRelatedStates (selectedAddress) {
const accountTokens = this.store.getState().accountTokens
if (!selectedAddress) selectedAddress = this.store.getState().selectedAddress
const providerType = this.network.providerStore.getState().type
if (!(selectedAddress in accountTokens)) accountTokens[selectedAddress] = {}
if (!(providerType in accountTokens[selectedAddress])) accountTokens[selectedAddress][providerType] = []
const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress }
}
}

module.exports = PreferencesController
3 changes: 2 additions & 1 deletion app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
network: this.networkController,
})

// currency controller
Expand Down Expand Up @@ -1436,7 +1437,7 @@ module.exports = class MetamaskController extends EventEmitter {
}

/**
* A method for activating the retrieval of price data and auto detect tokens,
* A method for activating the retrieval of price data,
* which should only be fetched when the UI is visible.
* @private
* @param {boolean} active - True if price data should be getting fetched.
Expand Down
40 changes: 40 additions & 0 deletions app/scripts/migrations/028.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// next version number
const version = 28

/*
normalizes txParams on unconfirmed txs
*/
const clone = require('clone')

module.exports = {
version,

migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
return versionedData
},
}

function transformState (state) {
const newState = state

if (newState.PreferencesController) {
if (newState.PreferencesController.tokens) {
const identities = newState.TransactionController.identities
const tokens = newState.PreferencesController.tokens
newState.PreferencesController.accountTokens = {}
for (const identity in identities) {
newState.PreferencesController.accountTokens[identity] = {'mainnet': tokens}
}
newState.PreferencesController.tokens = []
}
}

return newState
}
1 change: 1 addition & 0 deletions app/scripts/migrations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ module.exports = [
require('./025'),
require('./026'),
require('./027'),
require('./028'),
]
21 changes: 4 additions & 17 deletions test/unit/app/controllers/detect-tokens-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ const PreferencesController = require('../../../../app/scripts/controllers/prefe

describe('DetectTokensController', () => {
const sandbox = sinon.createSandbox()
let clock
let keyringMemStore
before(async () => {
let clock, keyringMemStore, network, preferences
beforeEach(async () => {
keyringMemStore = new ObservableStore({ isUnlocked: false})
network = new NetworkController({ provider: { type: 'mainnet' }})
preferences = new PreferencesController({ network })
})
after(() => {
sandbox.restore()
Expand All @@ -25,9 +26,7 @@ describe('DetectTokensController', () => {

it('should be called on every polling period', async () => {
clock = sandbox.useFakeTimers()
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
Expand All @@ -45,9 +44,7 @@ describe('DetectTokensController', () => {
})

it('should not check tokens while in test network', async () => {
const network = new NetworkController()
network.setProviderType('rinkeby')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
Expand All @@ -61,9 +58,7 @@ describe('DetectTokensController', () => {
})

it('should only check and add tokens while in main network', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
Expand All @@ -80,9 +75,7 @@ describe('DetectTokensController', () => {
})

it('should not detect same token while in main network', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
Expand All @@ -100,9 +93,7 @@ describe('DetectTokensController', () => {
})

it('should trigger detect new tokens when change address', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
Expand All @@ -112,9 +103,7 @@ describe('DetectTokensController', () => {
})

it('should trigger detect new tokens when submit password', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.selectedAddress = '0x0'
Expand All @@ -124,9 +113,7 @@ describe('DetectTokensController', () => {
})

it('should not trigger detect new tokens when not open or not unlocked', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = false
Expand Down
Loading

0 comments on commit e9f74f0

Please sign in to comment.