mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +01:00
* detect tokens polling * network store to detect token * tests for spec * passtest-lint * fix lint * improve tests * detect tokens through infura * detect tokens when submit password and new account selected * keyring unlocked detect and unit tests * add changelog
This commit is contained in:
parent
ad0f344af6
commit
cb045fd8fe
@ -2,6 +2,7 @@
|
||||
|
||||
## Current Master
|
||||
|
||||
- Add new tokens auto detection
|
||||
- Remove rejected transactions from transaction history
|
||||
- Add Trezor Support
|
||||
- Allow to remove accounts (Imported and Hardware Wallets)
|
||||
|
123
app/scripts/controllers/detect-tokens.js
Normal file
123
app/scripts/controllers/detect-tokens.js
Normal file
@ -0,0 +1,123 @@
|
||||
const Web3 = require('web3')
|
||||
const contracts = require('eth-contract-metadata')
|
||||
const { warn } = require('loglevel')
|
||||
const { MAINNET } = require('./network/enums')
|
||||
// By default, poll every 3 minutes
|
||||
const DEFAULT_INTERVAL = 180 * 1000
|
||||
const ERC20_ABI = [{'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}], 'name': 'balanceOf', 'outputs': [{'name': 'balance', 'type': 'uint256'}], 'payable': false, 'type': 'function'}]
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
this.web3.setProvider(this._network._provider)
|
||||
for (const contractAddress in contracts) {
|
||||
if (contracts[contractAddress].erc20 && !(this.tokenAddresses.includes(contractAddress.toLowerCase()))) {
|
||||
this.detectTokenBalance(contractAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find if selectedAddress has tokens with contract in contractAddress.
|
||||
*
|
||||
* @param {string} contractAddress Hex address of the token contract to explore.
|
||||
* @returns {boolean} If balance is detected, token is added.
|
||||
*
|
||||
*/
|
||||
async detectTokenBalance (contractAddress) {
|
||||
const ethContract = this.web3.eth.contract(ERC20_ABI).at(contractAddress)
|
||||
ethContract.balanceOf(this.selectedAddress, (error, result) => {
|
||||
if (!error) {
|
||||
if (!result.isZero()) {
|
||||
this._preferences.addToken(contractAddress, contracts[contractAddress].symbol, contracts[contractAddress].decimals)
|
||||
}
|
||||
} else {
|
||||
warn(`MetaMask - DetectTokensController balance fetch failed for ${contractAddress}.`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart token detection polling period and call detectNewTokens
|
||||
* in case of address change or user session initialization.
|
||||
*
|
||||
*/
|
||||
restartTokenDetection () {
|
||||
if (this.isActive && this.selectedAddress) {
|
||||
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
|
||||
preferences.store.subscribe(({ tokens }) => { this.tokenAddresses = tokens.map((obj) => { return obj.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) {
|
||||
if (isUnlocked) { this.restartTokenDetection() }
|
||||
this.isUnlocked = isUnlocked
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DetectTokensController
|
@ -35,6 +35,7 @@ const TypedMessageManager = require('./lib/typed-message-manager')
|
||||
const TransactionController = require('./controllers/transactions')
|
||||
const BalancesController = require('./controllers/computed-balances')
|
||||
const TokenRatesController = require('./controllers/token-rates')
|
||||
const DetectTokensController = require('./controllers/detect-tokens')
|
||||
const ConfigManager = require('./lib/config-manager')
|
||||
const nodeify = require('./lib/nodeify')
|
||||
const accountImporter = require('./account-import-strategies')
|
||||
@ -147,6 +148,13 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
this.accountTracker.syncWithAddresses(addresses)
|
||||
})
|
||||
|
||||
// detect tokens controller
|
||||
this.detectTokensController = new DetectTokensController({
|
||||
preferences: this.preferencesController,
|
||||
network: this.networkController,
|
||||
keyringMemStore: this.keyringController.memStore,
|
||||
})
|
||||
|
||||
// address book controller
|
||||
this.addressBookController = new AddressBookController({
|
||||
initState: initState.AddressBookController,
|
||||
@ -1420,11 +1428,13 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* A method for activating the retrieval of price data, which should only be fetched when the UI is visible.
|
||||
* A method for activating the retrieval of price data and auto detect tokens,
|
||||
* which should only be fetched when the UI is visible.
|
||||
* @private
|
||||
* @param {boolean} active - True if price data should be getting fetched.
|
||||
*/
|
||||
set isClientOpenAndUnlocked (active) {
|
||||
this.tokenRatesController.isActive = active
|
||||
this.detectTokensController.isActive = active
|
||||
}
|
||||
}
|
||||
|
120
test/unit/app/controllers/detect-tokens-test.js
Normal file
120
test/unit/app/controllers/detect-tokens-test.js
Normal file
@ -0,0 +1,120 @@
|
||||
const assert = require('assert')
|
||||
const sinon = require('sinon')
|
||||
const ObservableStore = require('obs-store')
|
||||
const DetectTokensController = require('../../../../app/scripts/controllers/detect-tokens')
|
||||
const NetworkController = require('../../../../app/scripts/controllers/network/network')
|
||||
const PreferencesController = require('../../../../app/scripts/controllers/preferences')
|
||||
|
||||
describe('DetectTokensController', () => {
|
||||
const sandbox = sinon.createSandbox()
|
||||
let clock
|
||||
let keyringMemStore
|
||||
before(async () => {
|
||||
keyringMemStore = new ObservableStore({ isUnlocked: false})
|
||||
})
|
||||
after(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it('should poll on correct interval', async () => {
|
||||
const stub = sinon.stub(global, 'setInterval')
|
||||
new DetectTokensController({ interval: 1337 }) // eslint-disable-line no-new
|
||||
assert.strictEqual(stub.getCall(0).args[1], 1337)
|
||||
stub.restore()
|
||||
})
|
||||
|
||||
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.isActive = true
|
||||
|
||||
var stub = sandbox.stub(controller, 'detectNewTokens')
|
||||
|
||||
clock.tick(1)
|
||||
sandbox.assert.notCalled(stub)
|
||||
clock.tick(180000)
|
||||
sandbox.assert.called(stub)
|
||||
clock.tick(180000)
|
||||
sandbox.assert.calledTwice(stub)
|
||||
clock.tick(180000)
|
||||
sandbox.assert.calledThrice(stub)
|
||||
})
|
||||
|
||||
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.isActive = true
|
||||
|
||||
var stub = sandbox.stub(controller, 'detectTokenBalance')
|
||||
.withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4').returns(true)
|
||||
.withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388').returns(true)
|
||||
|
||||
await controller.detectNewTokens()
|
||||
sandbox.assert.notCalled(stub)
|
||||
})
|
||||
|
||||
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.isActive = true
|
||||
|
||||
sandbox.stub(controller, 'detectTokenBalance')
|
||||
.withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4')
|
||||
.returns(preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8))
|
||||
.withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388')
|
||||
.returns(preferences.addToken('0xbc86727e770de68b1060c91f6bb6945c73e10388', 'XNK', 18))
|
||||
|
||||
await controller.detectNewTokens()
|
||||
assert.deepEqual(preferences.store.getState().tokens, [{address: '0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', decimals: 8, symbol: 'J8T'},
|
||||
{address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK'}])
|
||||
})
|
||||
|
||||
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.isActive = true
|
||||
|
||||
sandbox.stub(controller, 'detectTokenBalance')
|
||||
.withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4')
|
||||
.returns(preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8))
|
||||
.withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388')
|
||||
.returns(preferences.addToken('0xbc86727e770de68b1060c91f6bb6945c73e10388', 'XNK', 18))
|
||||
|
||||
await controller.detectNewTokens()
|
||||
assert.deepEqual(preferences.store.getState().tokens, [{address: '0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', decimals: 8, symbol: 'J8T'},
|
||||
{address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK'}])
|
||||
})
|
||||
|
||||
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.isActive = true
|
||||
var stub = sandbox.stub(controller, 'detectNewTokens')
|
||||
await preferences.setSelectedAddress('0xbc86727e770de68b1060c91f6bb6945c73e10388')
|
||||
sandbox.assert.called(stub)
|
||||
})
|
||||
|
||||
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.isActive = true
|
||||
controller.selectedAddress = '0x0'
|
||||
var stub = sandbox.stub(controller, 'detectNewTokens')
|
||||
await controller._keyringMemStore.updateState({ isUnlocked: true })
|
||||
sandbox.assert.called(stub)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user