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

Save recent network balances in local storage #5843

Merged
merged 4 commits into from
Nov 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions app/scripts/controllers/cached-balances.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const ObservableStore = require('obs-store')
const extend = require('xtend')

/**
* @typedef {Object} CachedBalancesOptions
* @property {Object} accountTracker An {@code AccountTracker} reference
* @property {Function} getNetwork A function to get the current network
* @property {Object} initState The initial controller state
*/

/**
* Background controller responsible for maintaining
* a cache of account balances in local storage
*/
class CachedBalancesController {
/**
* Creates a new controller instance
*
* @param {CachedBalancesOptions} [opts] Controller configuration parameters
*/
constructor (opts = {}) {
const { accountTracker, getNetwork } = opts

this.accountTracker = accountTracker
this.getNetwork = getNetwork

const initState = extend({
cachedBalances: {},
}, opts.initState)
this.store = new ObservableStore(initState)

this._registerUpdates()
}

/**
* Updates the cachedBalances property for the current network. Cached balances will be updated to those in the passed accounts
* if balances in the passed accounts are truthy.
*
* @param {Object} obj The the recently updated accounts object for the current network
* @returns {Promise<void>}
*/
async updateCachedBalances ({ accounts }) {
const network = await this.getNetwork()
const balancesToCache = await this._generateBalancesToCache(accounts, network)
this.store.updateState({
cachedBalances: balancesToCache,
})
}

_generateBalancesToCache (newAccounts, currentNetwork) {
const { cachedBalances } = this.store.getState()
const currentNetworkBalancesToCache = { ...cachedBalances[currentNetwork] }

Object.keys(newAccounts).forEach(accountID => {
const account = newAccounts[accountID]

if (account.balance) {
currentNetworkBalancesToCache[accountID] = account.balance
}
})
const balancesToCache = {
...cachedBalances,
[currentNetwork]: currentNetworkBalancesToCache,
}

return balancesToCache
}

/**
* Sets up listeners and subscriptions which should trigger an update of cached balances. These updates will
* happen when the current account changes. Which happens on block updates, as well as on network and account
* selections.
*
* @private
*
*/
_registerUpdates () {
const update = this.updateCachedBalances.bind(this)
this.accountTracker.store.subscribe(update)
}
}

module.exports = CachedBalancesController
9 changes: 9 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const ShapeShiftController = require('./controllers/shapeshift')
const AddressBookController = require('./controllers/address-book')
const InfuraController = require('./controllers/infura')
const BlacklistController = require('./controllers/blacklist')
const CachedBalancesController = require('./controllers/cached-balances')
const RecentBlocksController = require('./controllers/recent-blocks')
const MessageManager = require('./lib/message-manager')
const PersonalMessageManager = require('./lib/personal-message-manager')
Expand Down Expand Up @@ -142,6 +143,12 @@ module.exports = class MetamaskController extends EventEmitter {
}
})

this.cachedBalancesController = new CachedBalancesController({
accountTracker: this.accountTracker,
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
initState: initState.CachedBalancesController,
})

// ensure accountTracker updates balances after network change
this.networkController.on('networkDidChange', () => {
this.accountTracker._updateAccounts()
Expand Down Expand Up @@ -241,13 +248,15 @@ module.exports = class MetamaskController extends EventEmitter {
ShapeShiftController: this.shapeshiftController.store,
NetworkController: this.networkController.store,
InfuraController: this.infuraController.store,
CachedBalancesController: this.cachedBalancesController.store,
})

this.memStore = new ComposableObservableStore(null, {
NetworkController: this.networkController.store,
AccountTracker: this.accountTracker.store,
TxController: this.txController.memStore,
BalancesController: this.balancesController.store,
CachedBalancesController: this.cachedBalancesController.store,
TokenRatesController: this.tokenRatesController.store,
MessageManager: this.messageManager.memStore,
PersonalMessageManager: this.personalMessageManager.memStore,
Expand Down
137 changes: 137 additions & 0 deletions test/unit/app/controllers/cached-balances-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
const assert = require('assert')
const sinon = require('sinon')
const CachedBalancesController = require('../../../../app/scripts/controllers/cached-balances')

describe('CachedBalancesController', () => {
describe('updateCachedBalances', () => {
it('should update the cached balances', async () => {
const controller = new CachedBalancesController({
getNetwork: () => Promise.resolve(17),
accountTracker: {
store: {
subscribe: () => {},
},
},
initState: {
cachedBalances: 'mockCachedBalances',
},
})

controller._generateBalancesToCache = sinon.stub().callsFake(() => Promise.resolve('mockNewCachedBalances'))

await controller.updateCachedBalances({ accounts: 'mockAccounts' })

assert.equal(controller._generateBalancesToCache.callCount, 1)
assert.deepEqual(controller._generateBalancesToCache.args[0], ['mockAccounts', 17])
assert.equal(controller.store.getState().cachedBalances, 'mockNewCachedBalances')
})
})

describe('_generateBalancesToCache', () => {
it('should generate updated account balances where the current network was updated', () => {
const controller = new CachedBalancesController({
accountTracker: {
store: {
subscribe: () => {},
},
},
initState: {
cachedBalances: {
17: {
a: '0x1',
b: '0x2',
c: '0x3',
},
16: {
a: '0xa',
b: '0xb',
c: '0xc',
},
},
},
})

const result = controller._generateBalancesToCache({
a: { balance: '0x4' },
b: { balance: null },
c: { balance: '0x5' },
}, 17)

assert.deepEqual(result, {
17: {
a: '0x4',
b: '0x2',
c: '0x5',
},
16: {
a: '0xa',
b: '0xb',
c: '0xc',
},
})
})

it('should generate updated account balances where the a new network was selected', () => {
const controller = new CachedBalancesController({
accountTracker: {
store: {
subscribe: () => {},
},
},
initState: {
cachedBalances: {
17: {
a: '0x1',
b: '0x2',
c: '0x3',
},
},
},
})

const result = controller._generateBalancesToCache({
a: { balance: '0x4' },
b: { balance: null },
c: { balance: '0x5' },
}, 16)

assert.deepEqual(result, {
17: {
a: '0x1',
b: '0x2',
c: '0x3',
},
16: {
a: '0x4',
c: '0x5',
},
})
})
})

describe('_registerUpdates', () => {
it('should subscribe to the account tracker with the updateCachedBalances method', async () => {
const subscribeSpy = sinon.spy()
const controller = new CachedBalancesController({
getNetwork: () => Promise.resolve(17),
accountTracker: {
store: {
subscribe: subscribeSpy,
},
},
})
subscribeSpy.resetHistory()

const updateCachedBalancesSpy = sinon.spy()
controller.updateCachedBalances = updateCachedBalancesSpy
controller._registerUpdates({ accounts: 'mockAccounts' })

assert.equal(subscribeSpy.callCount, 1)

subscribeSpy.args[0][0]()

assert.equal(updateCachedBalancesSpy.callCount, 1)
})
})

})
4 changes: 3 additions & 1 deletion ui/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const h = require('react-hyperscript')
const actions = require('./actions')
const classnames = require('classnames')
const log = require('loglevel')
const { getMetaMaskAccounts } = require('./selectors')

// init
const InitializeScreen = require('../../mascara/src/app/first-time').default
Expand Down Expand Up @@ -275,9 +276,10 @@ function mapStateToProps (state) {
loadingMessage,
} = appState

const accounts = getMetaMaskAccounts(state)

const {
identities,
accounts,
address,
keyrings,
isInitialized,
Expand Down
3 changes: 2 additions & 1 deletion ui/app/components/account-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Tooltip = require('../tooltip')
import Identicon from '../identicon'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import { PRIMARY } from '../../constants/common'
import { getMetaMaskAccounts } from '../../selectors'

const {
SETTINGS_ROUTE,
Expand Down Expand Up @@ -41,7 +42,7 @@ function mapStateToProps (state) {
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
keyrings: state.metamask.keyrings,
identities: state.metamask.identities,
accounts: state.metamask.accounts,
accounts: getMetaMaskAccounts(state),
}
}

Expand Down
4 changes: 2 additions & 2 deletions ui/app/components/balance-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import TokenBalance from './token-balance'
import Identicon from './identicon'
import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../constants/common'
const { getNativeCurrency, getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors')
const { getNativeCurrency, getAssetImages, conversionRateSelector, getCurrentCurrency, getMetaMaskAccounts } = require('../selectors')

const { formatBalance } = require('../util')

module.exports = connect(mapStateToProps)(BalanceComponent)

function mapStateToProps (state) {
const accounts = state.metamask.accounts
const accounts = getMetaMaskAccounts(state)
const network = state.metamask.network
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
const account = accounts[selectedAddress]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { isBalanceSufficient } from '../../send/send.utils'
import { conversionGreaterThan } from '../../../conversion-util'
import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants'
import { addressSlicer, valuesFor } from '../../../util'
import { getMetaMaskAccounts } from '../../../selectors'

const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
return {
Expand Down Expand Up @@ -47,11 +48,11 @@ const mapStateToProps = (state, props) => {
} = confirmTransaction
const { txParams = {}, lastGasPrice, id: transactionId } = txData
const { from: fromAddress, to: txParamsToAddress } = txParams
const accounts = getMetaMaskAccounts(state)
const {
conversionRate,
identities,
currentCurrency,
accounts,
selectedAddress,
selectedAddressTxList,
assetImages,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../../../actions')
const { getMetaMaskAccounts } = require('../../../../selectors')
const ConnectScreen = require('./connect-screen')
const AccountList = require('./account-list')
const { DEFAULT_ROUTE } = require('../../../../routes')
Expand Down Expand Up @@ -224,8 +225,9 @@ ConnectHardwareForm.propTypes = {

const mapStateToProps = state => {
const {
metamask: { network, selectedAddress, identities = {}, accounts = [] },
metamask: { network, selectedAddress, identities = {} },
} = state
const accounts = getMetaMaskAccounts(state)
const numberOfExistingAccounts = Object.keys(identities).length
const {
appState: { defaultHdPaths },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const connect = require('react-redux').connect
const actions = require('../../../../actions')
const FileInput = require('react-simple-file-input').default
const { DEFAULT_ROUTE } = require('../../../../routes')
const { getMetaMaskAccounts } = require('../../../../selectors')
const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
import Button from '../../../button'

Expand Down Expand Up @@ -136,7 +137,7 @@ JsonImportSubview.propTypes = {
const mapStateToProps = state => {
return {
error: state.appState.warning,
firstAddress: Object.keys(state.metamask.accounts)[0],
firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const PropTypes = require('prop-types')
const connect = require('react-redux').connect
const actions = require('../../../../actions')
const { DEFAULT_ROUTE } = require('../../../../routes')
const { getMetaMaskAccounts } = require('../../../../selectors')
import Button from '../../../button'

PrivateKeyImportView.contextTypes = {
Expand All @@ -22,7 +23,7 @@ module.exports = compose(
function mapStateToProps (state) {
return {
error: state.appState.warning,
firstAddress: Object.keys(state.metamask.accounts)[0],
firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
}
}

Expand Down
Loading