diff options
author | Thomas <thomas.b.huang@gmail.com> | 2018-07-26 13:38:44 +0800 |
---|---|---|
committer | Thomas <thomas.b.huang@gmail.com> | 2018-07-26 13:38:44 +0800 |
commit | 138858647ed28a8aaba4b7e18e387ea0b6af0889 (patch) | |
tree | 0bc936ee7316112d01e617a246a645e3914da949 /app/scripts | |
parent | fa02a6c7c65f6866998171881fd657570fe3fe7b (diff) | |
parent | a5d344a58223e029dc86bf33a8ca9357492765f3 (diff) | |
download | tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar.gz tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar.bz2 tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar.lz tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar.xz tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar.zst tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.zip |
Merge branch 'develop' into network-remove-provider-engine
Diffstat (limited to 'app/scripts')
-rw-r--r-- | app/scripts/background.js | 14 | ||||
-rw-r--r-- | app/scripts/contentscript.js | 3 | ||||
-rw-r--r-- | app/scripts/controllers/detect-tokens.js | 130 | ||||
-rw-r--r-- | app/scripts/controllers/network/enums.js | 3 | ||||
-rw-r--r-- | app/scripts/controllers/preferences.js | 24 | ||||
-rw-r--r-- | app/scripts/controllers/transactions/nonce-tracker.js | 13 | ||||
-rw-r--r-- | app/scripts/controllers/transactions/tx-state-manager.js | 6 | ||||
-rw-r--r-- | app/scripts/lib/account-tracker.js | 4 | ||||
-rw-r--r-- | app/scripts/lib/contracts/registrar.js | 1 | ||||
-rw-r--r-- | app/scripts/lib/contracts/resolver.js | 2 | ||||
-rw-r--r-- | app/scripts/lib/ipfsContent.js | 44 | ||||
-rw-r--r-- | app/scripts/lib/resolver.js | 71 | ||||
-rw-r--r-- | app/scripts/lib/setupRaven.js | 8 | ||||
-rw-r--r-- | app/scripts/lib/util.js | 2 | ||||
-rw-r--r-- | app/scripts/metamask-controller.js | 170 | ||||
-rw-r--r-- | app/scripts/migrations/027.js | 35 | ||||
-rw-r--r-- | app/scripts/migrations/index.js | 1 | ||||
-rw-r--r-- | app/scripts/platforms/extension.js | 67 | ||||
-rw-r--r-- | app/scripts/ui.js | 1 |
19 files changed, 574 insertions, 25 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js index e61d2f6ea..4cd6dac4c 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -26,6 +26,8 @@ const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') const EdgeEncryptor = require('./edge-encryptor') const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code') const getObjStructure = require('./lib/getObjStructure') +const ipfsContent = require('./lib/ipfsContent.js') + const { ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_NOTIFICATION, @@ -45,8 +47,8 @@ const notificationManager = new NotificationManager() global.METAMASK_NOTIFIER = notificationManager // setup sentry error reporting -const release = platform.getVersion() -const raven = setupRaven({ release }) +const releaseVersion = platform.getVersion() +const raven = setupRaven({ releaseVersion }) // browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser // Internet Explorer 6-11 @@ -54,6 +56,7 @@ const isIE = !!document.documentMode // Edge 20+ const isEdge = !isIE && !!window.StyleMedia +let ipfsHandle let popupIsOpen = false let notificationIsOpen = false const openMetamaskTabsIDs = {} @@ -69,6 +72,7 @@ initialize().catch(log.error) // setup metamask mesh testing container setupMetamaskMeshMetrics() + /** * An object representing a transaction, in whatever state it is in. * @typedef TransactionMeta @@ -158,6 +162,7 @@ async function initialize () { const initLangCode = await getFirstPreferredLangCode() await setupController(initState, initLangCode) log.debug('MetaMask initialization complete.') + ipfsHandle = ipfsContent(initState.NetworkController.provider) } // @@ -261,6 +266,11 @@ function setupController (initState, initLangCode) { }) global.metamaskController = controller + controller.networkController.on('networkDidChange', () => { + ipfsHandle && ipfsHandle.remove() + ipfsHandle = ipfsContent(controller.networkController.providerStore.getState()) + }) + // report failed transactions to Sentry controller.txController.on(`tx:status-update`, (txId, status) => { if (status !== 'failed') return diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index b35a70dd2..7b7114c35 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -177,6 +177,9 @@ function blacklistedDomainCheck () { 'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html', 'adyen.com', 'gravityforms.com', + 'harbourair.com', + 'ani.gamer.com.tw', + 'blueskybooking.com', ] var currentUrl = window.location.href var currentRegex diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js new file mode 100644 index 000000000..195ec918a --- /dev/null +++ b/app/scripts/controllers/detect-tokens.js @@ -0,0 +1,130 @@ +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)) { 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 + 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) { + this.isUnlocked = isUnlocked + if (isUnlocked) { this.restartTokenDetection() } + } + }) + } + + /** + * Internal isActive state + * @type {Object} + */ + get isActive () { + return this.isOpen && this.isUnlocked + } +} + +module.exports = DetectTokensController diff --git a/app/scripts/controllers/network/enums.js b/app/scripts/controllers/network/enums.js index 9da7f309c..3190eb37c 100644 --- a/app/scripts/controllers/network/enums.js +++ b/app/scripts/controllers/network/enums.js @@ -4,6 +4,7 @@ const KOVAN = 'kovan' const MAINNET = 'mainnet' const LOCALHOST = 'localhost' +const MAINNET_CODE = 1 const ROPSTEN_CODE = 3 const RINKEYBY_CODE = 4 const KOVAN_CODE = 42 @@ -13,13 +14,13 @@ const RINKEBY_DISPLAY_NAME = 'Rinkeby' const KOVAN_DISPLAY_NAME = 'Kovan' const MAINNET_DISPLAY_NAME = 'Main Ethereum Network' - module.exports = { ROPSTEN, RINKEBY, KOVAN, MAINNET, LOCALHOST, + MAINNET_CODE, ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE, diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index b314745f5..f6250dc16 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -86,6 +86,30 @@ class PreferencesController { } /** + * Removes an address from state + * + * @param {string} address A hex address + * @returns {string} the address that was removed + */ + removeAddress (address) { + const identities = this.store.getState().identities + if (!identities[address]) { + throw new Error(`${address} can't be deleted cause it was not found`) + } + delete identities[address] + this.store.updateState({ identities }) + + // If the selected account is no longer valid, + // select an arbitrary other account: + if (address === this.getSelectedAddress()) { + const selected = Object.keys(identities)[0] + this.setSelectedAddress(selected) + } + return address + } + + + /** * Adds addresses to the identities object without removing identities * * @param {string[]} addresses An array of hex addresses diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js index a27568843..421036368 100644 --- a/app/scripts/controllers/transactions/nonce-tracker.js +++ b/app/scripts/controllers/transactions/nonce-tracker.js @@ -120,19 +120,6 @@ class NonceTracker { return Number.isInteger(highest) ? highest + 1 : 0 } - _reduceTxListToUniqueNonces (txList) { - const reducedTxList = txList.reduce((reducedList, txMeta, index) => { - if (!index) return [txMeta] - const nonceMatches = txList.filter((txData) => { - return txMeta.txParams.nonce === txData.txParams.nonce - }) - if (nonceMatches.length > 1) return reducedList - reducedList.push(txMeta) - return reducedList - }, []) - return reducedTxList - } - _getHighestNonce (txList) { const nonces = txList.map((txMeta) => { const nonce = txMeta.txParams.nonce diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 0aae4774b..28a18ca2e 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -288,6 +288,7 @@ class TransactionStateManager extends EventEmitter { */ setTxStatusRejected (txId) { this._setTxStatus(txId, 'rejected') + this._removeTx(txId) } /** @@ -422,6 +423,11 @@ class TransactionStateManager extends EventEmitter { _saveTxList (transactions) { this.store.updateState({ transactions }) } + + _removeTx (txId) { + const transactionList = this.getFullTxList() + this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId)) + } } module.exports = TransactionStateManager diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 1a2354463..b7e2c7cbe 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -79,7 +79,7 @@ class AccountTracker { }) this.addAccounts(accountsToAdd) - this.removeAccounts(accountsToRemove) + this.removeAccount(accountsToRemove) } /** @@ -108,7 +108,7 @@ class AccountTracker { * @param {array} an array of hex addresses to stop tracking * */ - removeAccounts (addresses) { + removeAccount (addresses) { const accounts = this.store.getState().accounts // remove each state object addresses.forEach(address => { diff --git a/app/scripts/lib/contracts/registrar.js b/app/scripts/lib/contracts/registrar.js new file mode 100644 index 000000000..99ca24458 --- /dev/null +++ b/app/scripts/lib/contracts/registrar.js @@ -0,0 +1 @@ +module.exports = [{'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'resolver', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'owner', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'label', 'type': 'bytes32'}, {'name': 'owner', 'type': 'address'}], 'name': 'setSubnodeOwner', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'ttl', 'type': 'uint64'}], 'name': 'setTTL', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'ttl', 'outputs': [{'name': '', 'type': 'uint64'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'resolver', 'type': 'address'}], 'name': 'setResolver', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'owner', 'type': 'address'}], 'name': 'setOwner', 'outputs': [], 'payable': false, 'type': 'function'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'owner', 'type': 'address'}], 'name': 'Transfer', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'label', 'type': 'bytes32'}, {'indexed': false, 'name': 'owner', 'type': 'address'}], 'name': 'NewOwner', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'resolver', 'type': 'address'}], 'name': 'NewResolver', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'ttl', 'type': 'uint64'}], 'name': 'NewTTL', 'type': 'event'}] diff --git a/app/scripts/lib/contracts/resolver.js b/app/scripts/lib/contracts/resolver.js new file mode 100644 index 000000000..1bf3f90ce --- /dev/null +++ b/app/scripts/lib/contracts/resolver.js @@ -0,0 +1,2 @@ +module.exports = +[{'constant': true, 'inputs': [{'name': 'interfaceID', 'type': 'bytes4'}], 'name': 'supportsInterface', 'outputs': [{'name': '', 'type': 'bool'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentTypes', 'type': 'uint256'}], 'name': 'ABI', 'outputs': [{'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'name': 'setPubkey', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'content', 'outputs': [{'name': 'ret', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'addr', 'outputs': [{'name': 'ret', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'name': 'setABI', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'name', 'outputs': [{'name': 'ret', 'type': 'string'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'name', 'type': 'string'}], 'name': 'setName', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'hash', 'type': 'bytes32'}], 'name': 'setContent', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'pubkey', 'outputs': [{'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'addr', 'type': 'address'}], 'name': 'setAddr', 'outputs': [], 'payable': false, 'type': 'function'}, {'inputs': [{'name': 'ensAddr', 'type': 'address'}], 'payable': false, 'type': 'constructor'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'a', 'type': 'address'}], 'name': 'AddrChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'hash', 'type': 'bytes32'}], 'name': 'ContentChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'name', 'type': 'string'}], 'name': 'NameChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'contentType', 'type': 'uint256'}], 'name': 'ABIChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'x', 'type': 'bytes32'}, {'indexed': false, 'name': 'y', 'type': 'bytes32'}], 'name': 'PubkeyChanged', 'type': 'event'}] diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js new file mode 100644 index 000000000..5222151ea --- /dev/null +++ b/app/scripts/lib/ipfsContent.js @@ -0,0 +1,44 @@ +const extension = require('extensionizer') +const resolver = require('./resolver.js') + +module.exports = function (provider) { + function ipfsContent (details) { + const name = details.url.substring(7, details.url.length - 1) + let clearTime = null + extension.tabs.getSelected(null, tab => { + extension.tabs.update(tab.id, { url: 'loading.html' }) + + clearTime = setTimeout(() => { + return extension.tabs.update(tab.id, { url: '404.html' }) + }, 60000) + + resolver.resolve(name, provider).then(ipfsHash => { + clearTimeout(clearTime) + let url = 'https://ipfs.infura.io/ipfs/' + ipfsHash + return fetch(url, { method: 'HEAD' }).then(response => response.status).then(statusCode => { + if (statusCode !== 200) return extension.tabs.update(tab.id, { url: '404.html' }) + extension.tabs.update(tab.id, { url: url }) + }) + .catch(err => { + url = 'https://ipfs.infura.io/ipfs/' + ipfsHash + extension.tabs.update(tab.id, {url: url}) + return err + }) + }) + .catch(err => { + clearTimeout(clearTime) + const url = err === 'unsupport' ? 'unsupport' : 'error' + extension.tabs.update(tab.id, {url: `${url}.html?name=${name}`}) + }) + }) + return { cancel: true } + } + + extension.webRequest.onBeforeRequest.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']}) + + return { + remove () { + extension.webRequest.onBeforeRequest.removeListener(ipfsContent) + }, + } +} diff --git a/app/scripts/lib/resolver.js b/app/scripts/lib/resolver.js new file mode 100644 index 000000000..ff0fed161 --- /dev/null +++ b/app/scripts/lib/resolver.js @@ -0,0 +1,71 @@ +const namehash = require('eth-ens-namehash') +const multihash = require('multihashes') +const HttpProvider = require('ethjs-provider-http') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') +const registrarAbi = require('./contracts/registrar') +const resolverAbi = require('./contracts/resolver') + +function ens (name, provider) { + const eth = new Eth(new HttpProvider(getProvider(provider.type))) + const hash = namehash.hash(name) + const contract = new EthContract(eth) + const Registrar = contract(registrarAbi).at(getRegistrar(provider.type)) + return new Promise((resolve, reject) => { + if (provider.type === 'mainnet' || provider.type === 'ropsten') { + Registrar.resolver(hash).then((address) => { + if (address === '0x0000000000000000000000000000000000000000') { + reject(null) + } else { + const Resolver = contract(resolverAbi).at(address['0']) + return Resolver.content(hash) + } + }).then((contentHash) => { + if (contentHash['0'] === '0x0000000000000000000000000000000000000000000000000000000000000000') reject(null) + if (contentHash.ret !== '0x') { + const hex = contentHash['0'].substring(2) + const buf = multihash.fromHexString(hex) + resolve(multihash.toB58String(multihash.encode(buf, 'sha2-256'))) + } else { + reject(null) + } + }) + } else { + return reject('unsupport') + } + }) +} + +function getProvider (type) { + switch (type) { + case 'mainnet': + return 'https://mainnet.infura.io/' + case 'ropsten': + return 'https://ropsten.infura.io/' + default: + return 'http://localhost:8545/' + } +} + +function getRegistrar (type) { + switch (type) { + case 'mainnet': + return '0x314159265dd8dbb310642f98f50c066173c1259b' + case 'ropsten': + return '0x112234455c3a32fd11230c42e7bccd4a84e02010' + default: + return '0x0000000000000000000000000000000000000000' + } +} + +module.exports.resolve = function (name, provider) { + const path = name.split('.') + const topLevelDomain = path[path.length - 1] + if (topLevelDomain === 'eth' || topLevelDomain === 'test') { + return ens(name, provider) + } else { + return new Promise((resolve, reject) => { + reject(null) + }) + } +} diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js index 85a078bf3..9aa2dec0a 100644 --- a/app/scripts/lib/setupRaven.js +++ b/app/scripts/lib/setupRaven.js @@ -8,8 +8,10 @@ module.exports = setupRaven // Setup raven / sentry remote error reporting function setupRaven (opts) { - const { release } = opts + const { releaseVersion } = opts let ravenTarget + // detect brave + const isBrave = Boolean(window.chrome.ipcRenderer) if (METAMASK_DEBUG) { console.log('Setting up Sentry Remote Error Reporting: DEV') @@ -20,9 +22,11 @@ function setupRaven (opts) { } const client = Raven.config(ravenTarget, { - release, + releaseVersion, transport: function (opts) { + opts.data.extra.isBrave = isBrave const report = opts.data + try { // handle error-like non-error exceptions rewriteErrorLikeExceptions(report) diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 7ceb9da3c..2b3fe3d6e 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -28,7 +28,7 @@ function getStack () { * */ const getEnvironmentType = (url = window.location.href) => { - if (url.match(/popup.html(?:\?.+)*$/)) { + if (url.match(/popup.html(?:#.*)*$/)) { return ENVIRONMENT_TYPE_POPUP } else if (url.match(/home.html(?:\?.+)*$/) || url.match(/home.html(?:#.*)*$/)) { return ENVIRONMENT_TYPE_FULLSCREEN diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 3d7268395..12daf1603 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -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') @@ -46,6 +47,7 @@ const GWEI_BN = new BN('1000000000') const percentile = require('percentile') const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const log = require('loglevel') +const TrezorKeyring = require('eth-trezor-keyring') module.exports = class MetamaskController extends EventEmitter { @@ -124,7 +126,9 @@ module.exports = class MetamaskController extends EventEmitter { }) // key mgmt + const additionalKeyrings = [TrezorKeyring] this.keyringController = new KeyringController({ + keyringTypes: additionalKeyrings, initState: initState.KeyringController, getNetwork: this.networkController.getNetworkState.bind(this.networkController), encryptor: opts.encryptor || undefined, @@ -144,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, @@ -164,6 +175,13 @@ module.exports = class MetamaskController extends EventEmitter { }) this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts)) + this.txController.on(`tx:status-update`, (txId, status) => { + if (status === 'confirmed' || status === 'failed') { + const txMeta = this.txController.txStateManager.getTx(txId) + this.platform.showTransactionNotification(txMeta) + } + }) + // computed balances (accounting for pending transactions) this.balancesController = new BalancesController({ accountTracker: this.accountTracker, @@ -332,6 +350,7 @@ module.exports = class MetamaskController extends EventEmitter { markAccountsFound: this.markAccountsFound.bind(this), markPasswordForgotten: this.markPasswordForgotten.bind(this), unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this), + getGasPrice: (cb) => cb(null, this.getGasPrice()), // coinbase buyEth: this.buyEth.bind(this), @@ -344,8 +363,17 @@ module.exports = class MetamaskController extends EventEmitter { verifySeedPhrase: nodeify(this.verifySeedPhrase, this), clearSeedWordCache: this.clearSeedWordCache.bind(this), resetAccount: nodeify(this.resetAccount, this), + removeAccount: nodeify(this.removeAccount, this), importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this), + // hardware wallets + connectHardware: nodeify(this.connectHardware, this), + forgetDevice: nodeify(this.forgetDevice, this), + checkHardwareStatus: nodeify(this.checkHardwareStatus, this), + + // TREZOR + unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this), + // vault management submitPassword: nodeify(this.submitPassword, this), @@ -502,6 +530,127 @@ module.exports = class MetamaskController extends EventEmitter { } // + // Hardware + // + + /** + * Fetch account list from a trezor device. + * + * @returns [] accounts + */ + async connectHardware (deviceName, page) { + + switch (deviceName) { + case 'trezor': + const keyringController = this.keyringController + const oldAccounts = await keyringController.getAccounts() + let keyring = await keyringController.getKeyringsByType( + 'Trezor Hardware' + )[0] + if (!keyring) { + keyring = await this.keyringController.addNewKeyring('Trezor Hardware') + } + let accounts = [] + + switch (page) { + case -1: + accounts = await keyring.getPreviousPage() + break + case 1: + accounts = await keyring.getNextPage() + break + default: + accounts = await keyring.getFirstPage() + } + + // Merge with existing accounts + // and make sure addresses are not repeated + const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))] + this.accountTracker.syncWithAddresses(accountsToTrack) + return accounts + + default: + throw new Error('MetamaskController:connectHardware - Unknown device') + } + } + + /** + * Check if the device is unlocked + * + * @returns {Promise<boolean>} + */ + async checkHardwareStatus (deviceName) { + + switch (deviceName) { + case 'trezor': + const keyringController = this.keyringController + const keyring = await keyringController.getKeyringsByType( + 'Trezor Hardware' + )[0] + if (!keyring) { + return false + } + return keyring.isUnlocked() + default: + throw new Error('MetamaskController:checkHardwareStatus - Unknown device') + } + } + + /** + * Clear + * + * @returns {Promise<boolean>} + */ + async forgetDevice (deviceName) { + + switch (deviceName) { + case 'trezor': + const keyringController = this.keyringController + const keyring = await keyringController.getKeyringsByType( + 'Trezor Hardware' + )[0] + if (!keyring) { + throw new Error('MetamaskController:forgetDevice - Trezor Hardware keyring not found') + } + keyring.forgetDevice() + return true + default: + throw new Error('MetamaskController:forgetDevice - Unknown device') + } + } + + /** + * Imports an account from a trezor device. + * + * @returns {} keyState + */ + async unlockTrezorAccount (index) { + const keyringController = this.keyringController + const keyring = await keyringController.getKeyringsByType( + 'Trezor Hardware' + )[0] + if (!keyring) { + throw new Error('MetamaskController - No Trezor Hardware Keyring found') + } + + keyring.setAccountToUnlock(index) + const oldAccounts = await keyringController.getAccounts() + const keyState = await keyringController.addNewAccount(keyring) + const newAccounts = await keyringController.getAccounts() + this.preferencesController.setAddresses(newAccounts) + newAccounts.forEach(address => { + if (!oldAccounts.includes(address)) { + this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`) + this.preferencesController.setSelectedAddress(address) + } + }) + + const { identities } = this.preferencesController.store.getState() + return { ...keyState, identities } + } + + + // // Account Management // @@ -614,6 +763,23 @@ module.exports = class MetamaskController extends EventEmitter { } /** + * Removes an account from state / storage. + * + * @param {string[]} address A hex address + * + */ + async removeAccount (address) { + // Remove account from the preferences controller + this.preferencesController.removeAddress(address) + // Remove account from the account tracker controller + this.accountTracker.removeAccount(address) + // Remove account from the keyring + await this.keyringController.removeAccount(address) + return address + } + + + /** * Imports an account with the specified import strategy. * These are defined in app/scripts/account-import-strategies * Each strategy represents a different way of serializing an Ethereum key pair. @@ -1235,10 +1401,12 @@ module.exports = class MetamaskController extends EventEmitter { set isClientOpen (open) { this._isClientOpen = open this.isClientOpenAndUnlocked = this.getState().isUnlocked && open + this.detectTokensController.isOpen = open } /** - * 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. */ diff --git a/app/scripts/migrations/027.js b/app/scripts/migrations/027.js new file mode 100644 index 000000000..d6ebef580 --- /dev/null +++ b/app/scripts/migrations/027.js @@ -0,0 +1,35 @@ +// next version number +const version = 27 + +/* + +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.TransactionController) { + if (newState.TransactionController.transactions) { + const transactions = newState.TransactionController.transactions + newState.TransactionController.transactions = transactions.filter((txMeta) => txMeta.status !== 'rejected') + } + } + + return newState +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 04d90bfff..bd0005221 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -37,4 +37,5 @@ module.exports = [ require('./024'), require('./025'), require('./026'), + require('./027'), ] diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index f5cc255d1..0803164e8 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -1,4 +1,5 @@ const extension = require('extensionizer') +const explorerLink = require('etherscan-link').createExplorerLink class ExtensionPlatform { @@ -13,12 +14,21 @@ class ExtensionPlatform { extension.tabs.create({ url }) } + closeCurrentWindow () { + return extension.windows.getCurrent((windowDetails) => { + return extension.windows.remove(windowDetails.id) + }) + } + getVersion () { return extension.runtime.getManifest().version } - openExtensionInBrowser () { - const extensionURL = extension.runtime.getURL('home.html') + openExtensionInBrowser (route = null) { + let extensionURL = extension.runtime.getURL('home.html') + if (route) { + extensionURL += `#${route}` + } this.openWindow({ url: extensionURL }) } @@ -31,6 +41,59 @@ class ExtensionPlatform { cb(e) } } + + showTransactionNotification (txMeta) { + + const status = txMeta.status + if (status === 'confirmed') { + this._showConfirmedTransaction(txMeta) + } else if (status === 'failed') { + this._showFailedTransaction(txMeta) + } + } + + _showConfirmedTransaction (txMeta) { + + this._subscribeToNotificationClicked() + + const url = explorerLink(txMeta.hash, parseInt(txMeta.metamaskNetworkId)) + const nonce = parseInt(txMeta.txParams.nonce, 16) + + const title = 'Confirmed transaction' + const message = `Transaction ${nonce} confirmed! View on EtherScan` + this._showNotification(title, message, url) + } + + _showFailedTransaction (txMeta) { + + const nonce = parseInt(txMeta.txParams.nonce, 16) + const title = 'Failed transaction' + const message = `Transaction ${nonce} failed! ${txMeta.err.message}` + this._showNotification(title, message) + } + + _showNotification (title, message, url) { + extension.notifications.create( + url, + { + 'type': 'basic', + 'title': title, + 'iconUrl': extension.extension.getURL('../../images/icon-64.png'), + 'message': message, + }) + } + + _subscribeToNotificationClicked () { + if (!extension.notifications.onClicked.hasListener(this._viewOnEtherScan)) { + extension.notifications.onClicked.addListener(this._viewOnEtherScan) + } + } + + _viewOnEtherScan (txId) { + if (txId.startsWith('http://')) { + global.metamaskController.platform.openWindow({ url: txId }) + } + } } module.exports = ExtensionPlatform diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 9bf97be87..da100f928 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -64,7 +64,6 @@ async function start () { css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss() deleteInjectedCss = injectCss(css) } - if (state.appState.shouldClose) notificationManager.closePopup() }) }) |