aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts
diff options
context:
space:
mode:
authorThomas <thomas.b.huang@gmail.com>2018-07-26 13:38:44 +0800
committerThomas <thomas.b.huang@gmail.com>2018-07-26 13:38:44 +0800
commit138858647ed28a8aaba4b7e18e387ea0b6af0889 (patch)
tree0bc936ee7316112d01e617a246a645e3914da949 /app/scripts
parentfa02a6c7c65f6866998171881fd657570fe3fe7b (diff)
parenta5d344a58223e029dc86bf33a8ca9357492765f3 (diff)
downloadtangerine-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.js14
-rw-r--r--app/scripts/contentscript.js3
-rw-r--r--app/scripts/controllers/detect-tokens.js130
-rw-r--r--app/scripts/controllers/network/enums.js3
-rw-r--r--app/scripts/controllers/preferences.js24
-rw-r--r--app/scripts/controllers/transactions/nonce-tracker.js13
-rw-r--r--app/scripts/controllers/transactions/tx-state-manager.js6
-rw-r--r--app/scripts/lib/account-tracker.js4
-rw-r--r--app/scripts/lib/contracts/registrar.js1
-rw-r--r--app/scripts/lib/contracts/resolver.js2
-rw-r--r--app/scripts/lib/ipfsContent.js44
-rw-r--r--app/scripts/lib/resolver.js71
-rw-r--r--app/scripts/lib/setupRaven.js8
-rw-r--r--app/scripts/lib/util.js2
-rw-r--r--app/scripts/metamask-controller.js170
-rw-r--r--app/scripts/migrations/027.js35
-rw-r--r--app/scripts/migrations/index.js1
-rw-r--r--app/scripts/platforms/extension.js67
-rw-r--r--app/scripts/ui.js1
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()
})
})