diff options
author | Thomas Huang <tmashuang@users.noreply.github.com> | 2018-12-04 07:16:19 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-12-04 07:16:19 +0800 |
commit | ed9bfdcebd5eed1d749f275f9d388ea0dd8f8275 (patch) | |
tree | 786b66ff556bc30a7f6136ba130e889408dcebb4 | |
parent | be3619cd802536894097d81e7f31d38b0c2b3e9f (diff) | |
parent | 35670e926116b19e66931dace838d785adffac09 (diff) | |
download | tangerine-wallet-browser-ed9bfdcebd5eed1d749f275f9d388ea0dd8f8275.tar.gz tangerine-wallet-browser-ed9bfdcebd5eed1d749f275f9d388ea0dd8f8275.tar.zst tangerine-wallet-browser-ed9bfdcebd5eed1d749f275f9d388ea0dd8f8275.zip |
Merge pull request #5879 from MetaMask/develop
Version 5.1.0
58 files changed, 518 insertions, 123 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 7571955d7..f8c1937af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ ## Current Develop Branch +## 5.1.0 Mon Dec 03 2018 + +- [#5860](https://github.com/MetaMask/metamask-extension/pull/5860): Fixed an infinite spinner bug. +- [#5875](https://github.com/MetaMask/metamask-extension/pull/5875): Update phishing warning copy +- [#5863](https://github.com/MetaMask/metamask-extension/pull/5863): bugfix: normalize contract addresss when fetching exchange rates +- [#5843](https://github.com/MetaMask/metamask-extension/pull/5843): Use selector for state.metamask.accounts in all cases. + +## 5.0.4 Thu Nov 29 2018 + +- [#5840](https://github.com/MetaMask/metamask-extension/pull/5840): transactions/tx-gas-utils - add the acctual response for eth_getCode for NO_CONTRACT_ERROR's && add a debug object to simulationFailed +- [#5848](https://github.com/MetaMask/metamask-extension/pull/5848): Soften accusatory language on phishing warning +- [#5835](https://github.com/MetaMask/metamask-extension/pull/5835): Open full-screen UI on install +- Locked versions for some dependencies to avoid possible issues from event-stream hack. +- [#5831](https://github.com/MetaMask/metamask-extension/pull/5831): Hide app-header when provider request pending +- [#5786](https://github.com/MetaMask/metamask-extension/pull/5786): * transactions - autofill gasPrice for retry attempts with either the recomened gasprice or a %10 bump
+- [#5801](https://github.com/MetaMask/metamask-extension/pull/5801): transactions - ensure err is defined when setting tx failed +- [#5792](https://github.com/MetaMask/metamask-extension/pull/5792): Consider HW Wallets for signTypedMessage +- [#5829](https://github.com/MetaMask/metamask-extension/pull/5829): Show disabled cursor in .network-disabled state +- [#5827](https://github.com/MetaMask/metamask-extension/pull/5827): Trim whitespace from seed phrase during import +- [#5832](https://github.com/MetaMask/metamask-extension/pull/5832): Show Connect Requests count in extension badge +- [#5816](https://github.com/MetaMask/metamask-extension/pull/5816): Increase Token Symbol length to twelve +- [#5819](https://github.com/MetaMask/metamask-extension/pull/5819): With the EIP 1102 updates, MetaMask *does* now open itself when visiting some websites. Changed the wording here to clarify that MetaMask will not open itself to ask you for your seed phrase. +- [#5810](https://github.com/MetaMask/metamask-extension/pull/5810): Bump Node version to 8.13 +- [#5797](https://github.com/MetaMask/metamask-extension/pull/5797): Add Firefox and Brave support for Trezor +- [#5799](https://github.com/MetaMask/metamask-extension/pull/5799): Fix usage of setState in ConfirmTransactionBase#handleSubmit +- [#5798](https://github.com/MetaMask/metamask-extension/pull/5798): Show byte count for hex data on confirm screen +- [#5334](https://github.com/MetaMask/metamask-extension/pull/5334): Default to the new UI for first time users +- [#5791](https://github.com/MetaMask/metamask-extension/pull/5791): Bump eth-ledger-bridge-keyring + ## 5.0.3 Mon Nov 19 2018 - [#5547](https://github.com/MetaMask/metamask-extension/pull/5547): Bundle some ui dependencies separately to limit the build size of ui.js @@ -9,7 +9,9 @@ If you're a user seeking support, [here is our support site](https://metamask.ze [Mission Statement](./MISSION.md) -[Internal documentation](./docs#documentation) +[Documentation](https://metamask.github.io/metamask-docs/) + +[Internal Code Documentation](./docs#documentation) ## Developing Compatible Dapps diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index cac436b1b..2bce52cd9 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -828,8 +828,8 @@ "supportCenter": { "message": "Navštivte naše centrum podpory" }, - "symbolBetweenZeroTen": { - "message": "Symbol musí být mezi 0 a 10 znaky." + "symbolBetweenZeroTwelve": { + "message": "Symbol musí být mezi 0 a 12 znaky." }, "takesTooLong": { "message": "Trvá to dlouho?" diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 71421a4d1..25ec628f0 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -807,8 +807,8 @@ "supportCenter": { "message": "Gehe zu unserem Support Center" }, - "symbolBetweenZeroTen": { - "message": "Das Symbol muss zwischen 0 und 10 Zeichen haben." + "symbolBetweenZeroTwelve": { + "message": "Das Symbol muss zwischen 0 und 12 Zeichen haben." }, "takesTooLong": { "message": "Dauert es zu lang?" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 14810782a..0fe8e81cd 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1165,8 +1165,8 @@ "supportCenter": { "message": "Visit our Support Center" }, - "symbolBetweenZeroTen": { - "message": "Symbol must be between 0 and 10 characters." + "symbolBetweenZeroTwelve": { + "message": "Symbol must be between 0 and 12 characters." }, "takesTooLong": { "message": "Taking too long?" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 3043e2ac0..ae1c9559f 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -804,8 +804,8 @@ "supportCenter": { "message": "Visita nuestro centro de atención" }, - "symbolBetweenZeroTen": { - "message": "Símbolo debe ser entre 0 y 10 caracteres" + "symbolBetweenZeroTwelve": { + "message": "Símbolo debe ser entre 0 y 12 caracteres" }, "takesTooLong": { "message": "¿Está tardando demasiado?" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 3e89afcc9..e60aa3218 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1120,8 +1120,8 @@ "supportCenter": { "message": "Visitez notre centre d'aide" }, - "symbolBetweenZeroTen": { - "message": "Le symbol doit avoir entre 0 et 10 caractères." + "symbolBetweenZeroTwelve": { + "message": "Le symbol doit avoir entre 0 et 12 caractères." }, "takesTooLong": { "message": "Cela prend trop de temps ?" diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index d9e74800c..4face7bd6 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -218,7 +218,7 @@ }, "decimal": { "message": "दशमलव परिशुद्धता" - }, + }, "defaultNetwork": { "message": "ईथर लेनदेन के लिए डिफ़ॉल्ट नेटवर्क मुख्य नेट है।" }, @@ -741,8 +741,8 @@ "supportCenter": { "message": "हमारे सहायता केंद्र पर जाएं" }, - "symbolBetweenZeroTen": { - "message": "प्रतीक 0 और 10 अक्षरों के बीच होना चाहिए" + "symbolBetweenZeroTwelve": { + "message": "प्रतीक 0 और 12 अक्षरों के बीच होना चाहिए" }, "takesTooLong": { "message": "बहुत समय ले रहा है?" @@ -845,7 +845,7 @@ }, "visitWebSite": { "message": "हमारी वेब साइट पर जाएं" - }, + }, "warning": { "message": "चेतावनी" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 30ed2621f..c94ddc8d2 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -1141,8 +1141,8 @@ "supportCenter": { "message": "Vizite Sant Sipò Nou" }, - "symbolBetweenZeroTen": { - "message": "Senbòl yo dwe ant 0 ak 10 karaktè." + "symbolBetweenZeroTwelve": { + "message": "Senbòl yo dwe ant 0 ak 12 karaktè." }, "takesTooLong": { "message": "Pran twò lontan?" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 70630f237..58b3dfc05 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1123,8 +1123,8 @@ "supportCenter": { "message": "Visita il nostro Centro di Supporto" }, - "symbolBetweenZeroTen": { - "message": "Il simbolo deve essere lungo tra 0 e 10 caratteri." + "symbolBetweenZeroTwelve": { + "message": "Il simbolo deve essere lungo tra 0 e 12 caratteri." }, "takesTooLong": { "message": "Ci sta mettendo troppo?" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index cadb0da06..4c6541d21 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1114,8 +1114,8 @@ "supportCenter": { "message": "지원 센터에 방문하기" }, - "symbolBetweenZeroTen": { - "message": "심볼은 0에서 10개 사이의 문자여야 합니다." + "symbolBetweenZeroTwelve": { + "message": "심볼은 0에서 12개 사이의 문자여야 합니다." }, "takesTooLong": { "message": "너무 오래 걸리나요?" diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index 3a041327b..b7b06e075 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -741,8 +741,8 @@ "supportCenter": { "message": "Bezoek ons ondersteuningscentrum" }, - "symbolBetweenZeroTen": { - "message": "Het symbool moet tussen 0 en 10 tekens lang zijn." + "symbolBetweenZeroTwelve": { + "message": "Het symbool moet tussen 0 en 12 tekens lang zijn." }, "takesTooLong": { "message": "Duurt te lang?" diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index ddd4131bb..7e64ec689 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -1042,8 +1042,8 @@ "supportCenter": { "message": "Odwiedź nasze Centrum Pomocy" }, - "symbolBetweenZeroTen": { - "message": "Symbol musi mieć od 0 do 10 znaków." + "symbolBetweenZeroTwelve": { + "message": "Symbol musi mieć od 0 do 12 znaków." }, "takesTooLong": { "message": "Trwa zbyt długo?" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 4e3465936..08af4b2a6 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -741,8 +741,8 @@ "supportCenter": { "message": "Visitar o nosso Centro de Suporte" }, - "symbolBetweenZeroTen": { - "message": "Símbolo deve conter entre 0 e 10 characters." + "symbolBetweenZeroTwelve": { + "message": "Símbolo deve conter entre 0 e 12 characters." }, "takesTooLong": { "message": "A demorar muito?" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 8788d9d92..9568c64e6 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -816,8 +816,8 @@ "supportCenter": { "message": "Перейти в наш Центр поддержки" }, - "symbolBetweenZeroTen": { - "message": "Символ должен быть от 0 до 10 символов." + "symbolBetweenZeroTwelve": { + "message": "Символ должен быть от 0 до 12 символов." }, "takesTooLong": { "message": "Слишком долго?" diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index fde553c9e..febcc9141 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -828,8 +828,8 @@ "supportCenter": { "message": "Navštivte naše centrum podpory" }, - "symbolBetweenZeroTen": { - "message": "Symbol musí být mezi 0 a 10 znaky." + "symbolBetweenZeroTwelve": { + "message": "Symbol musí být mezi 0 a 12 znaky." }, "takesTooLong": { "message": "Trvá to dlouho?" diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 7b8c7debd..1808dca27 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -1159,8 +1159,8 @@ "supportCenter": { "message": "Obiščite naše središče za podporo" }, - "symbolBetweenZeroTen": { - "message": "Simbol mora imeti med 0 in 10 znakov." + "symbolBetweenZeroTwelve": { + "message": "Simbol mora imeti med 0 in 12 znakov." }, "takesTooLong": { "message": "Trava predolgo?" diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index 9fb2d6fb0..b80c39b98 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -741,8 +741,8 @@ "supportCenter": { "message": "ไปที่ศูนย์สนับสนุนของเรา" }, - "symbolBetweenZeroTen": { - "message": "สัญลักษณ์ต้องมีความยาวตั้งแต่ 0 ถึง 10 อักขระ" + "symbolBetweenZeroTwelve": { + "message": "สัญลักษณ์ต้องมีความยาวตั้งแต่ 0 ถึง 12 อักขระ" }, "takesTooLong": { "message": "ใช้เวลานานเกินไปใช่หรือไม่?" diff --git a/app/_locales/tml/messages.json b/app/_locales/tml/messages.json index 022754da5..03ae4b7ff 100644 --- a/app/_locales/tml/messages.json +++ b/app/_locales/tml/messages.json @@ -828,8 +828,8 @@ "supportCenter": { "message": "எங்கள் ஆதரவு மையத்தைப் பார்வையிடவும்" }, - "symbolBetweenZeroTen": { - "message": "குறியீடு 0 மற்றும் 10 எழுத்துகளுக்கு இடையில் இருக்க வேண்டும்." + "symbolBetweenZeroTwelve": { + "message": "குறியீடு 0 மற்றும் 12 எழுத்துகளுக்கு இடையில் இருக்க வேண்டும்." }, "takesTooLong": { "message": "நீண்ட நேரம் எடுத்துக்கொள்கிறது?" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 66d5303d4..b085828a2 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -828,8 +828,8 @@ "supportCenter": { "message": "Destek merkezimizi ziyaret edin" }, - "symbolBetweenZeroTen": { - "message": "Sembol 0 ve 10 karakter aralığında olmalıdır." + "symbolBetweenZeroTwelve": { + "message": "Sembol 0 ve 12 karakter aralığında olmalıdır." }, "takesTooLong": { "message": "Çok mu uzun sürüyor?" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index e253aeb88..9d929d9a3 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -864,8 +864,8 @@ "supportCenter": { "message": "访问我们的支持中心" }, - "symbolBetweenZeroTen": { - "message": "符号应该有0-10个字符." + "symbolBetweenZeroTwelve": { + "message": "符号应该有0-12个字符." }, "takesTooLong": { "message": "花费太长时间?" diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index de79f935f..2d82a8acf 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -822,8 +822,8 @@ "supportCenter": { "message": "造訪我們的協助中心" }, - "symbolBetweenZeroTen": { - "message": "代號必須介於 0 到 10 字元間." + "symbolBetweenZeroTwelve": { + "message": "代號必須介於 0 到 12 字元間." }, "takesTooLong": { "message": "花費太長時間?" diff --git a/app/manifest.json b/app/manifest.json index c74d88448..07b3a3c48 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "5.0.3", + "version": "5.1.0", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", @@ -85,4 +85,4 @@ "*" ] } -} +}
\ No newline at end of file diff --git a/app/phishing.html b/app/phishing.html index 309021dc2..284d0fcac 100644 --- a/app/phishing.html +++ b/app/phishing.html @@ -55,14 +55,13 @@ <img src="/images/ethereum-metamask-chrome.png" style="width:100%"> <h3>ATTENTION</h3> - <p>MetaMask believes this domain could currently compromise your security and has prevented you from interacting with it.</p> - <p>This is because the site tested positive on the <a href="https://github.com/metamask/eth-phishing-detect">Ethereum Phishing Detector</a>. This includes outright malicious websites and legitimate websites that have been compromised by a malicious actor.</p> + <p>This domain is currently on the MetaMask domain warning list. This means that based on information available to us, MetaMask believes this domain could currently compromise your security and, as an added safety feature, MetaMask has restricted access to the site. To override this, please read the rest of this warning for instructions on how to continue at your own risk. </p> + <p>There are many reasons sites can appear on our warning list, and our warning list compiles from other widely used industry lists. Such reasons can include known fraud or security risks, such as domains that test positive on the <a href="https://github.com/metamask/eth-phishing-detect">Ethereum Phishing Detector</a>. Domains on these warning lists may include outright malicious websites and legitimate websites that have been compromised by a malicious actor. <p id="esdbLink"></p> - <p>You can turn MetaMask off to interact with this site, but it is advised not to.</p> + <p>Note that this warning list is compiled on a voluntary basis. This list may be inaccurate or incomplete. Just because a domain does not appear on this list is not an implicit guarantee of that domain's safety. As always, your transactions are your own responsibility. If you wish to interact with any domain on our warning list, you can do so by <a id="unsafe-continue">continuing at your own risk</a>.</p> <p> If you think this domain is incorrectly flagged or if a blocked legitimate website has resolved its security issues, - <a href="https://github.com/metamask/eth-phishing-detect/issues/new">please file an issue</a>. If you believe this website - is safe and understand the risks involved, you can <a id="unsafe-continue">visit this unsafe website at your own risk</a>. + <a href="https://github.com/metamask/eth-phishing-detect/issues/new">please file an issue</a>. </p> </div> diff --git a/app/scripts/background.js b/app/scripts/background.js index d577ead41..6056962dd 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -41,6 +41,7 @@ const { const firstTimeState = Object.assign({}, rawFirstTimeState, global.METAMASK_TEST_CONFIG) const STORAGE_KEY = 'metamask-config' +const METAMASK_DEBUG = process.env.METAMASK_DEBUG log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') @@ -266,7 +267,6 @@ function setupController (initState, initLangCode) { platform, encryptor: isEdge ? new EdgeEncryptor() : undefined, }) - global.metamaskController = controller const provider = controller.provider setupEnsIpfsResolver({ provider }) @@ -414,6 +414,7 @@ function setupController (initState, initLangCode) { controller.messageManager.on('updateBadge', updateBadge) controller.personalMessageManager.on('updateBadge', updateBadge) controller.typedMessageManager.on('updateBadge', updateBadge) + controller.providerApprovalController.store.on('update', updateBadge) /** * Updates the Web Extension's "badge" number, on the little fox in the toolbar. @@ -425,7 +426,8 @@ function setupController (initState, initLangCode) { var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount var unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount var unapprovedTypedMsgs = controller.typedMessageManager.unapprovedTypedMessagesCount - var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs + const pendingProviderRequests = controller.providerApprovalController.store.getState().providerRequests.length + var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs + pendingProviderRequests if (count) { label = String(count) } @@ -470,3 +472,10 @@ function openPopup () { } ) } + +// On first install, open a new tab with MetaMask +extension.runtime.onInstalled.addListener(({reason}) => { + if ((reason === 'install') && (!METAMASK_DEBUG)) { + platform.openExtensionInBrowser() + } +}) diff --git a/app/scripts/controllers/cached-balances.js b/app/scripts/controllers/cached-balances.js new file mode 100644 index 000000000..925c45334 --- /dev/null +++ b/app/scripts/controllers/cached-balances.js @@ -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 diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 21d7fd22e..53172c069 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -22,7 +22,9 @@ class ProviderApprovalController { this.platform = platform this.preferencesController = preferencesController this.publicConfigStore = publicConfigStore - this.store = new ObservableStore() + this.store = new ObservableStore({ + providerRequests: [], + }) if (platform && platform.addMessageListener) { platform.addMessageListener(({ action = '', force, origin, siteTitle, siteImage }) => { @@ -103,7 +105,7 @@ class ProviderApprovalController { */ approveProviderRequest (origin) { this.closePopup && this.closePopup() - const requests = this.store.getState().providerRequests || [] + const requests = this.store.getState().providerRequests this.platform && this.platform.sendMessage({ action: 'approve-provider-request', selectedAddress: this.publicConfigStore.getState().selectedAddress, @@ -121,7 +123,7 @@ class ProviderApprovalController { */ rejectProviderRequest (origin) { this.closePopup && this.closePopup() - const requests = this.store.getState().providerRequests || [] + const requests = this.store.getState().providerRequests this.platform && this.platform.sendMessage({ action: 'reject-provider-request' }, { active: true }) const providerRequests = requests.filter(request => request.origin !== origin) this.store.updateState({ providerRequests }) diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index a8936f13b..3f9482856 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -1,5 +1,6 @@ const ObservableStore = require('obs-store') const log = require('loglevel') +const normalizeAddress = require('eth-sig-util').normalize // By default, poll every 3 minutes const DEFAULT_INTERVAL = 180 * 1000 @@ -35,7 +36,8 @@ class TokenRatesController { const response = await fetch(`https://exchanges.balanc3.net/pie?${query}&autoConversion=true`) const { prices = [] } = await response.json() prices.forEach(({ pair, price }) => { - contractExchangeRates[pair.split('/')[0]] = typeof price === 'number' ? price : 0 + const address = pair.split('/')[0] + contractExchangeRates[normalizeAddress(address)] = typeof price === 'number' ? price : 0 }) } catch (error) { log.warn(`MetaMask - TokenRatesController exchange rate fetch failed.`, error) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index b44f66f14..9cd8429fb 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -96,7 +96,10 @@ class TransactionController extends EventEmitter { // memstore is computed from a few different stores this._updateMemstore() this.txStateManager.store.subscribe(() => this._updateMemstore()) - this.networkStore.subscribe(() => this._updateMemstore()) + this.networkStore.subscribe(() => { + this._onBootCleanUp() + this._updateMemstore() + }) this.preferencesStore.subscribe(() => this._updateMemstore()) // request state update to finalize initialization @@ -191,10 +194,13 @@ class TransactionController extends EventEmitter { txMeta = await this.addTxGasDefaults(txMeta) } catch (error) { log.warn(error) - this.txStateManager.setTxStatusFailed(txMeta.id, error) + txMeta.loadingDefaults = false + this.txStateManager.updateTx(txMeta, 'Failed to calculate gas defaults.') throw error } + txMeta.loadingDefaults = false + // save txMeta this.txStateManager.updateTx(txMeta) @@ -229,7 +235,16 @@ class TransactionController extends EventEmitter { async retryTransaction (originalTxId) { const originalTxMeta = this.txStateManager.getTx(originalTxId) + const { txParams } = originalTxMeta const lastGasPrice = originalTxMeta.txParams.gasPrice + const suggestedGasPriceBN = new ethUtil.BN(ethUtil.stripHexPrefix(this.getGasPrice()), 16) + const lastGasPriceBN = new ethUtil.BN(ethUtil.stripHexPrefix(lastGasPrice), 16) + // essentially lastGasPrice * 1.1 but + // dont trust decimals so a round about way of doing that + const lastGasPriceBNBumped = lastGasPriceBN.mul(new ethUtil.BN(110, 10)).div(new ethUtil.BN(100, 10)) + // transactions that are being retried require a >=%10 bump or the clients will throw an error + txParams.gasPrice = suggestedGasPriceBN.gt(lastGasPriceBNBumped) ? `0x${suggestedGasPriceBN.toString(16)}` : `0x${lastGasPriceBNBumped.toString(16)}` + const txMeta = this.txStateManager.generateTxMeta({ txParams: originalTxMeta.txParams, lastGasPrice, @@ -476,6 +491,8 @@ class TransactionController extends EventEmitter { txMeta.loadingDefaults = false this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') }).catch((error) => { + tx.loadingDefaults = false + this.txStateManager.updateTx(tx, 'failed to estimate gas during boot cleanup.') this.txStateManager.setTxStatusFailed(tx.id, error) }) }) diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index def67c2c3..b296dc5eb 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -35,7 +35,13 @@ class TxGasUtil { txMeta.simulationFails = { reason: err.message, errorKey: err.errorKey, + debug: { blockNumber: block.number, blockGasLimit: block.gasLimit }, } + + if (err.errorKey === TRANSACTION_NO_CONTRACT_ERROR_KEY) { + txMeta.simulationFails.debug.getCodeResponse = err.getCodeResponse + } + return txMeta } this.setTxGas(txMeta, block.gasLimit, estimatedGasHex) @@ -74,6 +80,9 @@ class TxGasUtil { const err = new Error('TxGasUtil - Trying to call a function on a non-contract address') // set error key so ui can display localized error message err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY + + // set the response on the error so that we can see in logs what the actual response was + err.getCodeResponse = code throw err } diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 151082452..72d869fa8 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -361,13 +361,15 @@ class TransactionStateManager extends EventEmitter { @param err {erroObject} - error object */ setTxStatusFailed (txId, err) { + const error = !err ? new Error('Internal metamask failure') : err + const txMeta = this.getTx(txId) txMeta.err = { - message: err.toString(), - rpc: err.value, - stack: err.stack, + message: error.toString(), + rpc: error.value, + stack: error.stack, } - this.updateTx(txMeta) + this.updateTx(txMeta, 'transactions:tx-state-manager#fail - add error') this._setTxStatus(txId, 'failed') } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index f3cd078b8..fe806e47e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -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') @@ -50,6 +51,7 @@ const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const log = require('loglevel') const TrezorKeyring = require('eth-trezor-keyring') const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring') +const HW_WALLETS_KEYRINGS = [TrezorKeyring.type, LedgerBridgeKeyring.type] const EthQuery = require('eth-query') const ethUtil = require('ethereumjs-util') const sigUtil = require('eth-sig-util') @@ -141,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() @@ -240,6 +248,7 @@ 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, { @@ -247,6 +256,7 @@ module.exports = class MetamaskController extends EventEmitter { 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, @@ -1025,16 +1035,22 @@ module.exports = class MetamaskController extends EventEmitter { const cleanMsgParams = await this.typedMessageManager.approveMessage(msgParams) const address = sigUtil.normalize(cleanMsgParams.from) const keyring = await this.keyringController.getKeyringForAccount(address) - const wallet = keyring._getWalletForAccount(address) - const privKey = ethUtil.toBuffer(wallet.getPrivateKey()) let signature - switch (version) { - case 'V1': - signature = sigUtil.signTypedDataLegacy(privKey, { data: cleanMsgParams.data }) - break - case 'V3': - signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) }) - break + // HW Wallet keyrings don't expose private keys + // so we need to handle it separately + if (!HW_WALLETS_KEYRINGS.includes(keyring.type)) { + const wallet = keyring._getWalletForAccount(address) + const privKey = ethUtil.toBuffer(wallet.getPrivateKey()) + switch (version) { + case 'V1': + signature = sigUtil.signTypedDataLegacy(privKey, { data: cleanMsgParams.data }) + break + case 'V3': + signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) }) + break + } + } else { + signature = await keyring.signTypedData(address, cleanMsgParams.data) } this.typedMessageManager.setMsgStatusSigned(msgId, signature) return this.getState() diff --git a/app/scripts/phishing-detect.js b/app/scripts/phishing-detect.js index 0889c831e..5ef99f181 100644 --- a/app/scripts/phishing-detect.js +++ b/app/scripts/phishing-detect.js @@ -1,7 +1,7 @@ window.onload = function () { if (window.location.pathname === '/phishing.html') { const {hostname} = parseHash() - document.getElementById('esdbLink').innerHTML = '<b>To read more about this scam, navigate to: <a href="https://etherscamdb.info/domain/' + hostname + '"> https://etherscamdb.info/domain/' + hostname + '</a></b>' + document.getElementById('esdbLink').innerHTML = '<b>To read more about this site and why it was blocked, <a href="https://etherscamdb.info/domain/' + hostname + '"> please navigate here</a>.</b>' } } diff --git a/mascara/src/app/first-time/import-seed-phrase-screen.js b/mascara/src/app/first-time/import-seed-phrase-screen.js index 883893e88..764e9ed4c 100644 --- a/mascara/src/app/first-time/import-seed-phrase-screen.js +++ b/mascara/src/app/first-time/import-seed-phrase-screen.js @@ -33,6 +33,7 @@ class ImportSeedPhraseScreen extends Component { parseSeedPhrase = (seedPhrase) => { return seedPhrase + .trim() .match(/\w+/g) .join(' ') } @@ -41,9 +42,10 @@ class ImportSeedPhraseScreen extends Component { let seedPhraseError = null if (seedPhrase) { - if (this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) { + const parsedSeedPhrase = this.parseSeedPhrase(seedPhrase) + if (parsedSeedPhrase.split(' ').length !== 12) { seedPhraseError = this.context.t('seedPhraseReq') - } else if (!validateMnemonic(seedPhrase)) { + } else if (!validateMnemonic(parsedSeedPhrase)) { seedPhraseError = this.context.t('invalidSeedPhrase') } } diff --git a/notices/archive/notice_4.md b/notices/archive/notice_4.md index c7a5f83a9..afe29eed2 100644 --- a/notices/archive/notice_4.md +++ b/notices/archive/notice_4.md @@ -1,6 +1,6 @@ Dear MetaMask Users, -There have been several instances of high-profile legitimate websites such as BTC Manager and Games Workshop that have had their websites temporarily compromised. This involves showing a fake MetaMask window on the page asking for user's seed phrases. MetaMask will never open itself in this way and users are encouraged to report these instances immediately to either [our phishing blacklist](https://github.com/MetaMask/eth-phishing-detect/issues) or our support email at [support@metamask.io](mailto:support@metamask.io). +There have been several instances of high-profile legitimate websites such as BTC Manager and Games Workshop that have had their websites temporarily compromised. This involves showing a fake MetaMask window on the page asking for user's seed phrases. MetaMask will never open itself to ask you for your seed phrase, and users are encouraged to report these instances immediately to either [our phishing blacklist](https://github.com/MetaMask/eth-phishing-detect/issues) or our support email at [support@metamask.io](mailto:support@metamask.io). Please read our full article on this ongoing issue at [https://medium.com/metamask/new-phishing-strategy-becoming-common-1b1123837168](https://medium.com/metamask/new-phishing-strategy-becoming-common-1b1123837168). diff --git a/package.json b/package.json index 996e11193..ecdf88202 100644 --- a/package.json +++ b/package.json @@ -276,7 +276,7 @@ "gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed", "gulp-babel": "^7.0.0", "gulp-json-editor": "^2.2.1", - "gulp-livereload": "^4.0.0", + "gulp-livereload": "4.0.0", "gulp-multi-process": "^1.3.1", "gulp-replace": "^0.6.1", "gulp-sourcemaps": "^2.6.0", @@ -326,7 +326,7 @@ "rimraf": "^2.6.2", "sass-loader": "^7.0.1", "selenium-webdriver": "^3.5.0", - "shell-parallel": "^1.0.3", + "shell-parallel": "1.0.3", "sinon": "^5.0.0", "source-map": "^0.7.2", "static-server": "^2.2.1", diff --git a/test/e2e/beta/from-import-beta-ui.spec.js b/test/e2e/beta/from-import-beta-ui.spec.js index 2b2e3361d..a180689e5 100644 --- a/test/e2e/beta/from-import-beta-ui.spec.js +++ b/test/e2e/beta/from-import-beta-ui.spec.js @@ -12,6 +12,7 @@ const { } = require('../func') const { checkBrowserForConsoleErrors, + closeAllWindowHandlesExcept, verboseReportOnFailure, findElement, findElements, @@ -32,13 +33,14 @@ describe('Using MetaMask with an existing account', function () { this.bail(true) before(async function () { + let extensionUrl switch (process.env.SELENIUM_BROWSER) { case 'chrome': { const extensionPath = path.resolve('dist/chrome') driver = buildChromeWebDriver(extensionPath) extensionId = await getExtensionIdChrome(driver) - await driver.get(`chrome-extension://${extensionId}/home.html`) await delay(regularDelayMs) + extensionUrl = `chrome-extension://${extensionId}/home.html` break } case 'firefox': { @@ -47,11 +49,17 @@ describe('Using MetaMask with an existing account', function () { await installWebExt(driver, extensionPath) await delay(regularDelayMs) extensionId = await getExtensionIdFirefox(driver) - await driver.get(`moz-extension://${extensionId}/home.html`) - await delay(regularDelayMs) + extensionUrl = `moz-extension://${extensionId}/home.html` break } } + // Depending on the state of the application built into the above directory (extPath) and the value of + // METAMASK_DEBUG we will see different post-install behaviour and possibly some extra windows. Here we + // are closing any extraneous windows to reset us to a single window before continuing. + const [tab1] = await driver.getAllWindowHandles() + await closeAllWindowHandlesExcept(driver, [tab1]) + await driver.switchTo().window(tab1) + await driver.get(extensionUrl) }) afterEach(async function () { diff --git a/test/e2e/beta/helpers.js b/test/e2e/beta/helpers.js index 5e3f45b2b..b6fc35e08 100644 --- a/test/e2e/beta/helpers.js +++ b/test/e2e/beta/helpers.js @@ -120,6 +120,13 @@ async function switchToWindowWithTitle (driver, title, windowHandles) { } } +/** + * Closes all windows except those in the given list of exceptions + * @param {object} driver the WebDriver instance + * @param {string|Array<string>} exceptions the list of window handle exceptions + * @param {Array?} windowHandles the full list of window handles + * @returns {Promise<void>} + */ async function closeAllWindowHandlesExcept (driver, exceptions, windowHandles) { exceptions = typeof exceptions === 'string' ? [ exceptions ] : exceptions windowHandles = windowHandles || await driver.getAllWindowHandles() diff --git a/test/e2e/beta/metamask-beta-responsive-ui.spec.js b/test/e2e/beta/metamask-beta-responsive-ui.spec.js index 107f9aa6c..8b34f9027 100644 --- a/test/e2e/beta/metamask-beta-responsive-ui.spec.js +++ b/test/e2e/beta/metamask-beta-responsive-ui.spec.js @@ -12,6 +12,7 @@ const { } = require('../func') const { checkBrowserForConsoleErrors, + closeAllWindowHandlesExcept, findElement, findElements, loadExtension, @@ -31,23 +32,33 @@ describe('MetaMask', function () { this.bail(true) before(async function () { + let extensionUrl switch (process.env.SELENIUM_BROWSER) { case 'chrome': { const extPath = path.resolve('dist/chrome') driver = buildChromeWebDriver(extPath, { responsive: true }) extensionId = await getExtensionIdChrome(driver) - await driver.get(`chrome-extension://${extensionId}/home.html`) + await delay(largeDelayMs) + extensionUrl = `chrome-extension://${extensionId}/home.html` break } case 'firefox': { const extPath = path.resolve('dist/firefox') driver = buildFirefoxWebdriver({ responsive: true }) await installWebExt(driver, extPath) - await delay(700) + await delay(largeDelayMs) extensionId = await getExtensionIdFirefox(driver) - await driver.get(`moz-extension://${extensionId}/home.html`) + extensionUrl = `moz-extension://${extensionId}/home.html` + break } } + // Depending on the state of the application built into the above directory (extPath) and the value of + // METAMASK_DEBUG we will see different post-install behaviour and possibly some extra windows. Here we + // are closing any extraneous windows to reset us to a single window before continuing. + const [tab1] = await driver.getAllWindowHandles() + await closeAllWindowHandlesExcept(driver, [tab1]) + await driver.switchTo().window(tab1) + await driver.get(extensionUrl) }) afterEach(async function () { diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index 9e96ceee6..e91af5303 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -37,23 +37,33 @@ describe('MetaMask', function () { this.bail(true) before(async function () { + let extensionUrl switch (process.env.SELENIUM_BROWSER) { case 'chrome': { const extPath = path.resolve('dist/chrome') driver = buildChromeWebDriver(extPath) extensionId = await getExtensionIdChrome(driver) - await driver.get(`chrome-extension://${extensionId}/home.html`) + await delay(largeDelayMs) + extensionUrl = `chrome-extension://${extensionId}/home.html` break } case 'firefox': { const extPath = path.resolve('dist/firefox') driver = buildFirefoxWebdriver() await installWebExt(driver, extPath) - await delay(700) + await delay(largeDelayMs) extensionId = await getExtensionIdFirefox(driver) - await driver.get(`moz-extension://${extensionId}/home.html`) + extensionUrl = `moz-extension://${extensionId}/home.html` + break } } + // Depending on the state of the application built into the above directory (extPath) and the value of + // METAMASK_DEBUG we will see different post-install behaviour and possibly some extra windows. Here we + // are closing any extraneous windows to reset us to a single window before continuing. + const [tab1] = await driver.getAllWindowHandles() + await closeAllWindowHandlesExcept(driver, [tab1]) + await driver.switchTo().window(tab1) + await driver.get(extensionUrl) }) afterEach(async function () { diff --git a/test/unit/app/controllers/cached-balances-test.js b/test/unit/app/controllers/cached-balances-test.js new file mode 100644 index 000000000..27aeabba2 --- /dev/null +++ b/test/unit/app/controllers/cached-balances-test.js @@ -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) + }) + }) + +}) diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index b76f256b9..2fe4c47d1 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -28,6 +28,7 @@ describe('Transaction Controller', function () { blockTrackerStub.getLatestBlock = noop txController = new TransactionController({ provider, + getGasPrice: function () { return '0xee6b2800' }, networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, blockTracker: blockTrackerStub, @@ -415,8 +416,9 @@ describe('Transaction Controller', function () { }) describe('#retryTransaction', function () { - it('should create a new txMeta with the same txParams as the original one', function (done) { + it('should create a new txMeta with the same txParams as the original one but with a higher gasPrice', function (done) { const txParams = { + gasPrice: '0xee6b2800', nonce: '0x00', from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4', to: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4', @@ -427,6 +429,7 @@ describe('Transaction Controller', function () { ]) txController.retryTransaction(1) .then((txMeta) => { + assert.equal(txMeta.txParams.gasPrice, '0x10642ac00', 'gasPrice should have a %10 gasPrice bump') assert.equal(txMeta.txParams.nonce, txParams.nonce, 'nonce should be the same') assert.equal(txMeta.txParams.from, txParams.from, 'from should be the same') assert.equal(txMeta.txParams.to, txParams.to, 'to should be the same') diff --git a/ui/app/app.js b/ui/app/app.js index b3aff1f39..5405f8495 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -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 @@ -275,9 +276,10 @@ function mapStateToProps (state) { loadingMessage, } = appState + const accounts = getMetaMaskAccounts(state) + const { identities, - accounts, address, keyrings, isInitialized, diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 94eae8d07..e88389096 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -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, @@ -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), } } diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js index 83ec4809d..83fcca620 100644 --- a/ui/app/components/app-header/app-header.component.js +++ b/ui/app/components/app-header/app-header.component.js @@ -23,6 +23,7 @@ export default class AppHeader extends PureComponent { toggleAccountMenu: PropTypes.func, selectedAddress: PropTypes.string, isUnlocked: PropTypes.bool, + providerRequests: PropTypes.array, } static contextTypes = { @@ -40,12 +41,23 @@ export default class AppHeader extends PureComponent { : hideNetworkDropdown() } + /** + * Returns whether or not the user is in the middle of a confirmation prompt + * + * This accounts for both tx confirmations as well as provider approvals + * + * @returns {boolean} + */ isConfirming () { - const { location } = this.props - - return Boolean(matchPath(location.pathname, { - path: CONFIRM_TRANSACTION_ROUTE, exact: false, - })) + const { location, providerRequests } = this.props + const confirmTxRouteMatch = matchPath(location.pathname, { + exact: false, + path: CONFIRM_TRANSACTION_ROUTE, + }) + const isConfirmingTx = Boolean(confirmTxRouteMatch) + const hasPendingProviderApprovals = Array.isArray(providerRequests) && providerRequests.length > 0 + + return isConfirmingTx || hasPendingProviderApprovals } renderAccountMenu () { diff --git a/ui/app/components/app-header/app-header.container.js b/ui/app/components/app-header/app-header.container.js index 30d3f8cc4..8b719bdf6 100644 --- a/ui/app/components/app-header/app-header.container.js +++ b/ui/app/components/app-header/app-header.container.js @@ -11,6 +11,7 @@ const mapStateToProps = state => { const { network, provider, + providerRequests, selectedAddress, isUnlocked, } = metamask @@ -19,6 +20,7 @@ const mapStateToProps = state => { networkDropdownOpen, network, provider, + providerRequests, selectedAddress, isUnlocked, } diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index 4e2769ee8..78b51449e 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -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] diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js index 3612e676c..82299bf88 100644 --- a/ui/app/components/pages/add-token/add-token.component.js +++ b/ui/app/components/pages/add-token/add-token.component.js @@ -194,8 +194,8 @@ class AddToken extends Component { const symbolLength = customSymbol.length let customSymbolError = null - if (symbolLength <= 0 || symbolLength >= 10) { - customSymbolError = this.context.t('symbolBetweenZeroTen') + if (symbolLength <= 0 || symbolLength >= 12) { + customSymbolError = this.context.t('symbolBetweenZeroTwelve') } this.setState({ customSymbol, customSymbolError }) diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js index 45bf62fb9..626143ac7 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -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 { @@ -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, diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index 4fe25f629..bd877fd4e 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -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') @@ -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 }, diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js index 90279bbbd..bb6771e4d 100644 --- a/ui/app/components/pages/create-account/import-account/json.js +++ b/ui/app/components/pages/create-account/import-account/json.js @@ -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' @@ -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], } } diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js index 8db1bfbdd..45068b96e 100644 --- a/ui/app/components/pages/create-account/import-account/private-key.js +++ b/ui/app/components/pages/create-account/import-account/private-key.js @@ -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 = { @@ -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], } } diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js index eb22a08b7..c217d848e 100644 --- a/ui/app/components/send/send.selectors.js +++ b/ui/app/components/send/send.selectors.js @@ -4,6 +4,9 @@ const { multiplyCurrencies, } = require('../../conversion-util') const { + getMetaMaskAccounts, +} = require('../../selectors') +const { estimateGasPriceFromRecentBlocks, } = require('./send.utils') @@ -54,10 +57,8 @@ const selectors = { module.exports = selectors function accountsWithSendEtherInfoSelector (state) { - const { - accounts, - identities, - } = state.metamask + const accounts = getMetaMaskAccounts(state) + const { identities } = state.metamask const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => { return Object.assign({}, account, identities[key]) @@ -72,7 +73,7 @@ function accountsWithSendEtherInfoSelector (state) { // const autoAddTokensThreshold = 1 // const numberOfTransactions = state.metamask.selectedAddressTxList.length -// const numberOfAccounts = Object.keys(state.metamask.accounts).length +// const numberOfAccounts = Object.keys(getMetaMaskAccounts(state)).length // const numberOfTokensAdded = state.metamask.tokens.length // const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) && @@ -155,14 +156,14 @@ function getRecentBlocks (state) { } function getSelectedAccount (state) { - const accounts = state.metamask.accounts + const accounts = getMetaMaskAccounts(state) const selectedAddress = getSelectedAddress(state) return accounts[selectedAddress] } function getSelectedAddress (state) { - const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0] + const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0] return selectedAddress } diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js index cb8078ec1..f9f05b0ae 100644 --- a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js +++ b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js @@ -2,12 +2,19 @@ import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { compose } from 'recompose' import TransactionViewBalance from './transaction-view-balance.component' -import { getSelectedToken, getSelectedAddress, getNativeCurrency, getSelectedTokenAssetImage } from '../../selectors' +import { + getSelectedToken, + getSelectedAddress, + getNativeCurrency, + getSelectedTokenAssetImage, + getMetaMaskAccounts, +} from '../../selectors' import { showModal } from '../../actions' const mapStateToProps = state => { const selectedAddress = getSelectedAddress(state) - const { metamask: { network, accounts } } = state + const { metamask: { network } } = state + const accounts = getMetaMaskAccounts(state) const account = accounts[selectedAddress] const { balance } = account diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index e050e0ee6..8ad6637ae 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -38,7 +38,7 @@ function mapStateToProps (state) { network: state.metamask.network, sidebarOpen: state.appState.sidebar.isOpen, identities: state.metamask.identities, - accounts: state.metamask.accounts, + accounts: selectors.getMetaMaskAccounts(state), tokens: state.metamask.tokens, keyrings: state.metamask.keyrings, selectedAddress: selectors.getSelectedAddress(state), diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 0784a872e..34f5466e2 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -12,6 +12,7 @@ const R = require('ramda') const SignatureRequest = require('./components/signature-request') const Loading = require('./components/loading-screen') const { DEFAULT_ROUTE } = require('./routes') +const { getMetaMaskAccounts } = require('./selectors') module.exports = compose( withRouter, @@ -28,7 +29,7 @@ function mapStateToProps (state) { return { identities: state.metamask.identities, - accounts: state.metamask.accounts, + accounts: getMetaMaskAccounts(state), selectedAddress: state.metamask.selectedAddress, unapprovedTxs: state.metamask.unapprovedTxs, unapprovedMsgs: state.metamask.unapprovedMsgs, diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss index 833a91f12..c828a2b26 100644 --- a/ui/app/css/itcss/components/network.scss +++ b/ui/app/css/itcss/components/network.scss @@ -1,6 +1,6 @@ .network-component--disabled { // border-color: transparent !important; - cursor: default; + cursor: not-allowed; .fa-caret-down { opacity: 0; diff --git a/ui/app/selectors.js b/ui/app/selectors.js index b518527c9..f99f14aa8 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -1,9 +1,7 @@ const abi = require('human-standard-token-abi') - import { transactionsSelector, } from './selectors/transactions' - const { multiplyCurrencies, } = require('./conversion-util') @@ -36,12 +34,13 @@ const selectors = { getCurrentViewContext, getTotalUnapprovedCount, preferencesSelector, + getMetaMaskAccounts, } module.exports = selectors function getSelectedAddress (state) { - const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0] + const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0] return selectedAddress } @@ -53,8 +52,27 @@ function getSelectedIdentity (state) { return identities[selectedAddress] } +function getMetaMaskAccounts (state) { + const currentAccounts = state.metamask.accounts + const cachedBalances = state.metamask.cachedBalances + const selectedAccounts = {} + + Object.keys(currentAccounts).forEach(accountID => { + const account = currentAccounts[accountID] + if (account && account.balance === null || account.balance === undefined) { + selectedAccounts[accountID] = { + ...account, + balance: cachedBalances[accountID], + } + } else { + selectedAccounts[accountID] = account + } + }) + return selectedAccounts +} + function getSelectedAccount (state) { - const accounts = state.metamask.accounts + const accounts = getMetaMaskAccounts(state) const selectedAddress = getSelectedAddress(state) return accounts[selectedAddress] @@ -102,10 +120,8 @@ function getAddressBook (state) { } function accountsWithSendEtherInfoSelector (state) { - const { - accounts, - identities, - } = state.metamask + const accounts = getMetaMaskAccounts(state) + const { identities } = state.metamask const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => { return Object.assign({}, account, identities[key]) @@ -175,7 +191,7 @@ function autoAddToBetaUI (state) { const autoAddTokensThreshold = 1 const numberOfTransactions = state.metamask.selectedAddressTxList.length - const numberOfAccounts = Object.keys(state.metamask.accounts).length + const numberOfAccounts = Object.keys(getMetaMaskAccounts(state)).length const numberOfTokensAdded = state.metamask.tokens.length const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) && |