mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
656dc4cf18
The tests for the detect-tokens controller were nearly all broken. They have been fixed, and a few improvements were made to controller itself to help with this. * The core `detectNewTokens` method has been updated to be async, so that the caller can know when the operation had completed. * The part of the function that used `Web3` to check the token balances has been split into a separate function, so that that part could be stubbed out in tests. Eventually we should test this using `ganache` instead, but this was an easier first step. * The internal `tokenAddresses` array is now initialized on construction, rather than upon the first Preferences controller update. The `detectNewTokens` function would have previously failed if it ran prior to this initialization, so it was failing if called before any preferences state changes. Additionally, the `detectTokenBalance` function was removed, as it was no longer used. The tests have been updated to ensure they're actually testing the behavior they purport to be testing. I've simulated a test failure with each one to check that it'd fail when it should. The preferences controller instance was updated to set addresses correctly as well.
167 lines
4.3 KiB
JavaScript
167 lines
4.3 KiB
JavaScript
import Web3 from 'web3'
|
|
import contracts from 'eth-contract-metadata'
|
|
import { warn } from 'loglevel'
|
|
import { MAINNET } from './network/enums'
|
|
// By default, poll every 3 minutes
|
|
const DEFAULT_INTERVAL = 180 * 1000
|
|
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'
|
|
|
|
const SINGLE_CALL_BALANCES_ADDRESS = '0xb1f8e55c7f64d203c1400b9d8555d050f94adf39'
|
|
/**
|
|
* A controller that polls for token exchange
|
|
* rates based on a user's current token list
|
|
*/
|
|
class DetectTokensController {
|
|
/**
|
|
* Creates a DetectTokensController
|
|
*
|
|
* @param {Object} [config] - Options to configure controller
|
|
*/
|
|
constructor ({ interval = DEFAULT_INTERVAL, preferences, network, keyringMemStore } = {}) {
|
|
this.preferences = preferences
|
|
this.interval = interval
|
|
this.network = network
|
|
this.keyringMemStore = keyringMemStore
|
|
}
|
|
|
|
/**
|
|
* For each token in eth-contract-metada, find check selectedAddress balance.
|
|
*
|
|
*/
|
|
async detectNewTokens () {
|
|
if (!this.isActive) {
|
|
return
|
|
}
|
|
if (this._network.store.getState().provider.type !== MAINNET) {
|
|
return
|
|
}
|
|
|
|
const tokensToDetect = []
|
|
this.web3.setProvider(this._network._provider)
|
|
for (const contractAddress in contracts) {
|
|
if (contracts[contractAddress].erc20 && !(this.tokenAddresses.includes(contractAddress.toLowerCase()))) {
|
|
tokensToDetect.push(contractAddress)
|
|
}
|
|
}
|
|
|
|
let result
|
|
try {
|
|
result = await this._getTokenBalances(tokensToDetect)
|
|
} catch (error) {
|
|
warn(`MetaMask - DetectTokensController single call balance fetch failed`, error)
|
|
return
|
|
}
|
|
|
|
tokensToDetect.forEach((tokenAddress, index) => {
|
|
const balance = result[index]
|
|
if (balance && !balance.isZero()) {
|
|
this._preferences.addToken(tokenAddress, contracts[tokenAddress].symbol, contracts[tokenAddress].decimals)
|
|
}
|
|
})
|
|
}
|
|
|
|
async _getTokenBalances (tokens) {
|
|
const ethContract = this.web3.eth.contract(SINGLE_CALL_BALANCES_ABI).at(SINGLE_CALL_BALANCES_ADDRESS)
|
|
return new Promise((resolve, reject) => {
|
|
ethContract.balances([this.selectedAddress], tokens, (error, result) => {
|
|
if (error) {
|
|
return reject(error)
|
|
}
|
|
return resolve(result)
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Restart token detection polling period and call detectNewTokens
|
|
* in case of address change or user session initialization.
|
|
*
|
|
*/
|
|
restartTokenDetection () {
|
|
if (!(this.isActive && this.selectedAddress)) {
|
|
return
|
|
}
|
|
this.detectNewTokens()
|
|
this.interval = DEFAULT_INTERVAL
|
|
}
|
|
|
|
/**
|
|
* @type {Number}
|
|
*/
|
|
set interval (interval) {
|
|
this._handle && clearInterval(this._handle)
|
|
if (!interval) {
|
|
return
|
|
}
|
|
this._handle = setInterval(() => {
|
|
this.detectNewTokens()
|
|
}, interval)
|
|
}
|
|
|
|
/**
|
|
* In setter when selectedAddress is changed, detectNewTokens and restart polling
|
|
* @type {Object}
|
|
*/
|
|
set preferences (preferences) {
|
|
if (!preferences) {
|
|
return
|
|
}
|
|
this._preferences = preferences
|
|
const currentTokens = preferences.store.getState().tokens
|
|
this.tokenAddresses = currentTokens
|
|
? currentTokens.map((token) => token.address)
|
|
: []
|
|
preferences.store.subscribe(({ tokens = [] }) => {
|
|
this.tokenAddresses = tokens.map((token) => {
|
|
return token.address
|
|
})
|
|
})
|
|
preferences.store.subscribe(({ selectedAddress }) => {
|
|
if (this.selectedAddress !== selectedAddress) {
|
|
this.selectedAddress = selectedAddress
|
|
this.restartTokenDetection()
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @type {Object}
|
|
*/
|
|
set network (network) {
|
|
if (!network) {
|
|
return
|
|
}
|
|
this._network = network
|
|
this.web3 = new Web3(network._provider)
|
|
}
|
|
|
|
/**
|
|
* In setter when isUnlocked is updated to true, detectNewTokens and restart polling
|
|
* @type {Object}
|
|
*/
|
|
set keyringMemStore (keyringMemStore) {
|
|
if (!keyringMemStore) {
|
|
return
|
|
}
|
|
this._keyringMemStore = keyringMemStore
|
|
this._keyringMemStore.subscribe(({ isUnlocked }) => {
|
|
if (this.isUnlocked !== isUnlocked) {
|
|
this.isUnlocked = isUnlocked
|
|
if (isUnlocked) {
|
|
this.restartTokenDetection()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Internal isActive state
|
|
* @type {Object}
|
|
*/
|
|
get isActive () {
|
|
return this.isOpen && this.isUnlocked
|
|
}
|
|
}
|
|
|
|
export default DetectTokensController
|